Skip to content

Instantly share code, notes, and snippets.

@Byteflux
Last active June 7, 2019 19:32
Show Gist options
  • Save Byteflux/1d2a4c0aa94eb5064e17e3674e11729b to your computer and use it in GitHub Desktop.
Save Byteflux/1d2a4c0aa94eb5064e17e3674e11729b to your computer and use it in GitHub Desktop.
LibraryLoader example
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);
}
}
}
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);
}
}
}
}
}
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);
}
}
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