Last active
June 7, 2019 19:32
-
-
Save Byteflux/1d2a4c0aa94eb5064e17e3674e11729b to your computer and use it in GitHub Desktop.
LibraryLoader example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.byteflux.core.library; | |
import java.util.ArrayList; | |
import java.util.Base64; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.LinkedList; | |
import java.util.List; | |
import static java.util.Objects.requireNonNull; | |
public class Library { | |
private final Collection<String> urls; | |
private final String groupId; | |
private final String artifactId; | |
private final String version; | |
private final String classifier; | |
private final byte[] checksum; | |
private final Collection<Relocation> relocations; | |
public Library(Collection<String> urls, | |
String groupId, | |
String artifactId, | |
String version, | |
String classifier, | |
byte[] checksum, | |
Collection<Relocation> relocations) { | |
this.urls = urls != null ? Collections.unmodifiableList(new LinkedList<>(urls)) : Collections.emptyList(); | |
this.groupId = requireNonNull(groupId, "groupId"); | |
this.artifactId = requireNonNull(artifactId, "artifactId"); | |
this.version = requireNonNull(version, "version"); | |
this.classifier = classifier; | |
this.checksum = requireNonNull(checksum, "checksum"); | |
this.relocations = relocations != null ? Collections.unmodifiableList(new LinkedList<>(relocations)) : null; | |
} | |
public Collection<String> getUrls() { | |
return urls; | |
} | |
public String getGroupId() { | |
return groupId; | |
} | |
public String getArtifactId() { | |
return artifactId; | |
} | |
public String getVersion() { | |
return version; | |
} | |
public String getClassifier() { | |
return classifier; | |
} | |
public byte[] getChecksum() { | |
return checksum; | |
} | |
public Collection<Relocation> getRelocations() { | |
return relocations; | |
} | |
public boolean hasRelocations() { | |
return !relocations.isEmpty(); | |
} | |
@Override | |
public String toString() { | |
return "Library{" + | |
"groupId='" + groupId + '\'' + | |
", artifactId='" + artifactId + '\'' + | |
", version='" + version + '\'' + | |
", classifier='" + classifier + '\'' + | |
'}'; | |
} | |
public static Builder builder() { | |
return new Builder(); | |
} | |
public static class Builder { | |
private final List<String> urls = new ArrayList<>(); | |
private String groupId; | |
private String artifactId; | |
private String version; | |
private String classifier; | |
private byte[] checksum; | |
private final List<Relocation> relocations = new ArrayList<>(); | |
private Builder() {} | |
public Builder url(String url) { | |
urls.add(requireNonNull(url, "url").toLowerCase()); | |
return this; | |
} | |
public Builder groupId(String groupId) { | |
this.groupId = requireNonNull(groupId, "groupId"); | |
return this; | |
} | |
public Builder artifactId(String artifactId) { | |
this.artifactId = requireNonNull(artifactId, "artifactId"); | |
return this; | |
} | |
public Builder version(String version) { | |
this.version = requireNonNull(version, "version"); | |
return this; | |
} | |
public Builder classifier(String classifier) { | |
this.classifier = classifier; | |
return this; | |
} | |
public Builder checksum(byte[] checksum) { | |
this.checksum = requireNonNull(checksum, "checksum"); | |
return this; | |
} | |
public Builder checksum(String checksum) { | |
this.checksum = Base64.getDecoder().decode(requireNonNull(checksum, "checksum")); | |
return this; | |
} | |
public Builder relocate(Relocation relocation) { | |
relocations.add(requireNonNull(relocation, "relocation")); | |
return this; | |
} | |
public Builder relocate(String pattern, String relocatedPattern) { | |
return relocate(new Relocation(pattern, relocatedPattern)); | |
} | |
public Library build() { | |
return new Library(urls, groupId, artifactId, version, classifier, checksum, relocations); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.byteflux.core.library; | |
import java.io.ByteArrayOutputStream; | |
import java.io.File; | |
import java.io.FileNotFoundException; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.lang.reflect.Constructor; | |
import java.lang.reflect.Method; | |
import java.net.MalformedURLException; | |
import java.net.SocketTimeoutException; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
import java.net.URLConnection; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Base64; | |
import java.util.Collection; | |
import java.util.LinkedList; | |
import java.util.List; | |
import java.util.logging.Level; | |
import org.bukkit.plugin.Plugin; | |
import static java.util.Objects.requireNonNull; | |
public class LibraryManager { | |
private static Method classLoaderAddUrlMethod; | |
private static Constructor<?> jarRelocatorConstructor; | |
private static Method jarRelocatorRunMethod; | |
private static Constructor<?> relocationConstructor; | |
private final Plugin plugin; | |
private final URLClassLoader classLoader; | |
private final Path saveDirectory; | |
private final List<String> repositories = new ArrayList<>(); | |
public LibraryManager(Plugin plugin) { | |
this.plugin = requireNonNull(plugin, "module"); | |
ClassLoader classLoader = plugin.getClass().getClassLoader(); | |
if (!(classLoader instanceof URLClassLoader)) { | |
throw new IllegalArgumentException("Unsupported class loader, URLClassLoader is required"); | |
} | |
this.classLoader = (URLClassLoader) classLoader; | |
saveDirectory = plugin.getDataFolder().toPath().resolve("lib"); | |
} | |
public ClassLoader getClassLoader() { | |
return classLoader; | |
} | |
public void addRepository(String url) { | |
repositories.add(requireNonNull(url, "url").endsWith("/") ? url.substring(0, url.length() - 1) : url); | |
} | |
public void relocate(Path input, Path output, Collection<Relocation> relocations) { | |
requireNonNull(input, "input"); | |
requireNonNull(output, "output"); | |
requireNonNull(relocations, "relocations"); | |
initializeJarRelocator(); | |
try { | |
List<Object> objects = new ArrayList<>(); | |
for (Relocation relocation : relocations) { | |
String pattern = relocation.getPattern(); | |
String relocatedPattern = relocation.getRelocatedPattern(); | |
Collection<String> includes = relocation.getIncludes(); | |
Collection<String> excludes = relocation.getExcludes(); | |
objects.add(relocationConstructor.newInstance(pattern, relocatedPattern, includes, excludes)); | |
} | |
jarRelocatorRunMethod.invoke(jarRelocatorConstructor.newInstance(input.toFile(), output.toFile(), objects)); | |
} catch (ReflectiveOperationException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
public Path downloadLibrary(Library library) { | |
String path = requireNonNull(library, "library").getGroupId().replace('.', '/') + '/' + library.getArtifactId() + '/' + library.getVersion(); | |
String name = library.getArtifactId() + '-' + library.getVersion() + ".jar"; | |
Path file = saveDirectory.resolve(path + '/' + name); | |
if (Files.exists(file)) { | |
return file; | |
} | |
String remotePath = path + '/' + library.getArtifactId() + '-' + library.getVersion(); | |
if (library.getClassifier() != null) { | |
remotePath += '-' + library.getClassifier(); | |
} | |
remotePath += ".jar"; | |
List<String> urls = new LinkedList<>(library.getUrls()); | |
for (String repository : repositories) { | |
urls.add(repository + '/' + remotePath); | |
} | |
urls.add("https://repo1.maven.org/maven2/" + remotePath); | |
MessageDigest digest; | |
try { | |
digest = MessageDigest.getInstance("SHA-256"); | |
} catch (NoSuchAlgorithmException e) { | |
throw new RuntimeException(e); | |
} | |
Path dir = file.getParent(); | |
for (String url : urls) { | |
plugin.getLogger().info("Downloading library " + url); | |
try { | |
URLConnection connection = new URL(url).openConnection(); | |
connection.setConnectTimeout(5000); | |
connection.setReadTimeout(5000); | |
connection.setRequestProperty("User-Agent", "LibraryManager"); | |
try (InputStream in = connection.getInputStream()) { | |
int len; | |
byte[] buf = new byte[4096]; | |
ByteArrayOutputStream out = new ByteArrayOutputStream(); | |
while ((len = in.read(buf)) != -1) { | |
out.write(buf, 0, len); | |
} | |
byte[] jar = out.toByteArray(); | |
byte[] checksum = digest.digest(jar); | |
if (!Arrays.equals(checksum, library.getChecksum())) { | |
plugin.getLogger().warning("** INVALID CHECKSUM **"); | |
plugin.getLogger().warning("Expected: " + Base64.getEncoder().encodeToString(library.getChecksum())); | |
plugin.getLogger().warning("Returned: " + Base64.getEncoder().encodeToString(checksum)); | |
continue; | |
} | |
Files.createDirectories(dir); | |
Path tmpFile = dir.resolve(name + ".tmp"); | |
tmpFile.toFile().deleteOnExit(); | |
try { | |
Files.write(tmpFile, jar); | |
Files.move(tmpFile, file); | |
return file; | |
} finally { | |
try { | |
Files.deleteIfExists(tmpFile); | |
} catch (IOException e) { | |
plugin.getLogger().log(Level.WARNING, "Couldn't delete temporary file: " + tmpFile, e); | |
} | |
} | |
} catch (SocketTimeoutException e) { | |
plugin.getLogger().warning(connection.getURL().getHost() + ": " + e.getMessage()); | |
} catch (FileNotFoundException ignored) { | |
} | |
} catch (IOException e) { | |
plugin.getLogger().log(Level.WARNING, "Couldn't download " + url + " due to an unexpected error!", e); | |
} | |
} | |
throw new LibraryNotFoundException(library.toString()); | |
} | |
public void loadLibrary(Path path) { | |
requireNonNull(path, "path"); | |
synchronized (LibraryManager.class) { | |
if (classLoaderAddUrlMethod == null) { | |
try { | |
classLoaderAddUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); | |
classLoaderAddUrlMethod.setAccessible(true); | |
} catch (NoSuchMethodException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} | |
try { | |
classLoaderAddUrlMethod.invoke(classLoader, path.toUri().toURL()); | |
} catch (MalformedURLException e) { | |
throw new IllegalArgumentException(e); | |
} catch (ReflectiveOperationException e) { | |
throw new IllegalStateException(e); | |
} | |
} | |
public void loadLibrary(Library library) { | |
Path file = downloadLibrary(requireNonNull(library, "library")); | |
if (library.hasRelocations()) { | |
String name = library.getArtifactId() + '-' + library.getVersion() + "-relocated.jar"; | |
Path dir = file.getParent(); | |
Path relocatedFile = dir.resolve(name); | |
if (!Files.exists(relocatedFile)) { | |
Path tmpFile = dir.resolve(name + ".tmp"); | |
try { | |
relocate(file, tmpFile, library.getRelocations()); | |
Files.move(tmpFile, relocatedFile); | |
file = relocatedFile; | |
} catch (IOException e) { | |
throw new RuntimeException(e); | |
} finally { | |
try { | |
Files.deleteIfExists(tmpFile); | |
} catch (IOException e) { | |
plugin.getLogger().log(Level.WARNING, "Couldn't delete temporary file: " + tmpFile, e); | |
} | |
} | |
} else { | |
file = relocatedFile; | |
} | |
} | |
loadLibrary(file); | |
} | |
/** | |
* Initializes Luck's Jar Relocator library used to perform jar relocations against downloaded libraries. | |
* | |
* The relocator library and its dependencies are automatically downloaded if needed. The relocator classes are | |
* loaded by an isolated class loader to prevent polluting the class space of other plugins. | |
*/ | |
private void initializeJarRelocator() { | |
synchronized (LibraryManager.class) { | |
if (jarRelocatorConstructor == null) { | |
try { | |
// Download the jar relocator library and its dependencies. | |
URL[] urls = new URL[] { | |
downloadLibrary( | |
Library.builder() | |
.groupId("org.ow2.asm") | |
.artifactId("asm") | |
.version("6.0") | |
.checksum("3Ylxx0pOaXiZqOlcquTqh2DqbEhtxrl7F5XnV2BCBGE=") | |
.build()).toUri().toURL(), | |
downloadLibrary( | |
Library.builder() | |
.groupId("org.ow2.asm") | |
.artifactId("asm-commons") | |
.version("6.0") | |
.checksum("8bzlxkipagF73NAf5dWa+YRSl/17ebgcAVpvu9lxmr8=") | |
.build()).toUri().toURL(), | |
downloadLibrary( | |
Library.builder() | |
.groupId("me.lucko") | |
.artifactId("jar-relocator") | |
.version("1.3") | |
.checksum("mmz3ltQbS8xXGA2scM0ZH6raISlt4nukjCiU2l9Jxfs=") | |
.build()).toUri().toURL(), | |
}; | |
// Create an isolated class loader to prevent conflicts with other plugins. | |
ClassLoader classLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader().getParent()); | |
Class<?> jarRelocatorClass = classLoader.loadClass("me.lucko.jarrelocator.JarRelocator"); | |
Class<?> relocationClass = classLoader.loadClass("me.lucko.jarrelocator.Relocation"); | |
Constructor<?> jarRelocatorConstructor = jarRelocatorClass.getConstructor(File.class, File.class, Collection.class); | |
Method jarRelocatorRunMethod = jarRelocatorClass.getMethod("run"); | |
Constructor<?> relocationConstructor = relocationClass.getConstructor(String.class, String.class, Collection.class, Collection.class); | |
// Finally we do the field assignments. | |
// To prevent possibly corrupting state, this should always be the last thing we do! | |
LibraryManager.jarRelocatorConstructor = jarRelocatorConstructor; | |
LibraryManager.jarRelocatorRunMethod = jarRelocatorRunMethod; | |
LibraryManager.relocationConstructor = relocationConstructor; | |
} catch (MalformedURLException | ReflectiveOperationException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.byteflux.core.library; | |
public class LibraryNotFoundException extends RuntimeException { | |
public LibraryNotFoundException(String message) { | |
super(message); | |
} | |
public LibraryNotFoundException(String message, Throwable cause) { | |
super(message, cause); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.byteflux.core.library; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.LinkedList; | |
import java.util.List; | |
import static java.util.Objects.requireNonNull; | |
public class Relocation { | |
private final String pattern; | |
private final String relocatedPattern; | |
private final Collection<String> includes; | |
private final Collection<String> excludes; | |
private Relocation(String pattern, String relocatedPattern, Collection<String> includes, Collection<String> excludes) { | |
this.pattern = requireNonNull(pattern, "pattern"); | |
this.relocatedPattern = requireNonNull(relocatedPattern, "relocatedPattern"); | |
this.includes = includes != null ? Collections.unmodifiableList(new LinkedList<>(includes)) : Collections.emptyList(); | |
this.excludes = excludes != null ? Collections.unmodifiableList(new LinkedList<>(excludes)) : Collections.emptyList(); | |
} | |
public Relocation(String pattern, String relocatedPattern) { | |
this(pattern, relocatedPattern, null, null); | |
} | |
public String getPattern() { | |
return pattern; | |
} | |
public String getRelocatedPattern() { | |
return relocatedPattern; | |
} | |
public Collection<String> getIncludes() { | |
return includes; | |
} | |
public Collection<String> getExcludes() { | |
return excludes; | |
} | |
public static Builder builder() { | |
return new Builder(); | |
} | |
public static class Builder { | |
private String pattern; | |
private String relocatedPattern; | |
private final List<String> includes = new ArrayList<>(); | |
private final List<String> excludes = new ArrayList<>(); | |
private Builder() {} | |
public Builder pattern(String pattern) { | |
this.pattern = requireNonNull(pattern, "pattern"); | |
return this; | |
} | |
public Builder relocatedPattern(String relocatedPattern) { | |
this.relocatedPattern = requireNonNull(relocatedPattern, "relocatedPattern"); | |
return this; | |
} | |
public Builder include(String include) { | |
includes.add(requireNonNull(include, "include")); | |
return this; | |
} | |
public Builder exclude(String exclude) { | |
excludes.add(requireNonNull(exclude, "exclude")); | |
return this; | |
} | |
public Relocation build() { | |
return new Relocation(pattern, relocatedPattern, includes, excludes); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment