Most common repositories can be found in {@link Repositories} class as constants. + *
Note that repositories should be preferably added to the {@link LibraryManager} via {@link LibraryManager#addRepository(String)}. + * + * @param url repository URL + * @return this builder + */ + public Builder repository(String url) { + repositories.add(requireNonNull(url, "repository").endsWith("/") ? url : url + '/'); + return this; + } + + /** + * Sets the id for this library. + * + * @param id the ID + * @return this builder + */ + public Builder id(String id) { + this.id = id != null ? id : UUID.randomUUID().toString(); + return this; + } + + /** + * Sets the Maven group ID for this library. + * + * @param groupId Maven group ID + * @return this builder + */ + public Builder groupId(String groupId) { + this.groupId = requireNonNull(groupId, "groupId"); + return this; + } + + /** + * Sets the Maven artifact ID for this library. + * + * @param artifactId Maven artifact ID + * @return this builder + */ + public Builder artifactId(String artifactId) { + this.artifactId = requireNonNull(artifactId, "artifactId"); + return this; + } + + /** + * Sets the artifact version for this library. + * + * @param version artifact version + * @return this builder + */ + public Builder version(String version) { + this.version = requireNonNull(version, "version"); + return this; + } + + /** + * Sets the artifact classifier for this library. + * + * @param classifier artifact classifier + * @return this builder + */ + public Builder classifier(String classifier) { + this.classifier = requireNonNull(classifier, "classifier"); + return this; + } + + /** + * Sets the binary SHA-256 checksum for this library. + * + * @param checksum binary SHA-256 checksum + * @return this builder + */ + public Builder checksum(byte[] checksum) { + this.checksum = requireNonNull(checksum, "checksum"); + return this; + } + + /** + * Sets the Base64-encoded SHA-256 checksum for this library. + * + * @param checksum Base64-encoded SHA-256 checksum + * @return this builder + */ + public Builder checksum(String checksum) { + return checksum(Base64.getDecoder().decode(requireNonNull(checksum, "checksum"))); + } + + /** + * Sets the isolated load for this library. + * + * @param isolatedLoad the isolated load boolean + * @return this builder + */ + public Builder isolatedLoad(boolean isolatedLoad) { + this.isolatedLoad = isolatedLoad; + return this; + } + + /** + * Adds a jar relocation to apply to this library. + * + * @param relocation jar relocation to apply + * @return this builder + */ + public Builder relocate(Relocation relocation) { + relocations.add(requireNonNull(relocation, "relocation")); + return this; + } + + /** + * Adds a jar relocation to apply to this library. + * + * @param pattern search pattern + * @param relocatedPattern replacement pattern + * @return this builder + */ + public Builder relocate(String pattern, String relocatedPattern) { + return relocate(new Relocation(pattern, relocatedPattern)); + } + + /** + * Creates a new library using this builder's configuration. + * + * @return new library + */ + public Library build() { + return new Library(urls, repositories, id, groupId, artifactId, version, classifier, checksum, relocations, isolatedLoad); + } + } +} diff --git a/src/main/java/net/byteflux/libby/LibraryManager.java b/src/main/java/net/byteflux/libby/LibraryManager.java new file mode 100644 index 0000000..9892996 --- /dev/null +++ b/src/main/java/net/byteflux/libby/LibraryManager.java @@ -0,0 +1,596 @@ +package net.byteflux.libby; + +import net.byteflux.libby.classloader.IsolatedClassLoader; +import net.byteflux.libby.logging.LogLevel; +import net.byteflux.libby.logging.Logger; +import net.byteflux.libby.logging.adapters.LogAdapter; +import net.byteflux.libby.relocation.Relocation; +import net.byteflux.libby.relocation.RelocationHelper; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +/** + * A runtime dependency manager for plugins. + *
+ * The library manager can resolve a dependency jar through the configured + * Maven repositories, download it into a local cache, relocate it and then + * load it into the plugin's classpath. + *
+ * Transitive dependencies for a library aren't downloaded automatically and + * must be explicitly loaded like every other library. + *
+ * It's recommended that libraries are relocated to prevent any namespace
+ * conflicts with different versions of the same library bundled with other
+ * plugins or maybe even bundled with the server itself.
+ *
+ * @see Library
+ */
+public abstract class LibraryManager {
+ /**
+ * Wrapped plugin logger
+ */
+ protected final Logger logger;
+
+ /**
+ * Directory where downloaded library jars are saved to
+ */
+ protected final Path saveDirectory;
+
+ /**
+ * Maven repositories used to resolve artifacts
+ */
+ private final Set
+ * By setting this value, the library manager's logger will not log any
+ * messages with a level less severe than the configured level. This can be
+ * useful for silencing the download and relocation logging.
+ *
+ * Setting this value to {@link LogLevel#WARN} would silence informational
+ * logging but still print important things like invalid checksum warnings.
+ *
+ * @param level the log level to set
+ */
+ public void setLogLevel(LogLevel level) {
+ logger.setLevel(level);
+ }
+
+ /**
+ * Gets the currently added repositories used to resolve artifacts.
+ *
+ * For each library this list is traversed to download artifacts after the
+ * direct download URLs have been attempted.
+ *
+ * @return current repositories
+ */
+ public Collection
+ * Artifacts will be resolved using this repository when attempts to locate
+ * the artifact through previously added repositories are all unsuccessful.
+ *
+ * @param url repository URL to add
+ */
+ public void addRepository(String url) {
+ String repo = requireNonNull(url, "url").endsWith("/") ? url : url + '/';
+ synchronized (repositories) {
+ repositories.add(repo);
+ }
+ }
+
+ /**
+ * Adds the current user's local Maven repository.
+ */
+ public void addMavenLocal() {
+ addRepository(Paths.get(System.getProperty("user.home")).resolve(".m2/repository").toUri().toString());
+ }
+
+ /**
+ * Adds the Maven Central repository.
+ */
+ public void addMavenCentral() {
+ addRepository(Repositories.MAVEN_CENTRAL);
+ }
+
+ /**
+ * Adds the Sonatype OSS repository.
+ */
+ public void addSonatype() {
+ addRepository(Repositories.SONATYPE);
+ }
+
+ /**
+ * Adds the Bintray JCenter repository.
+ */
+ public void addJCenter() {
+ addRepository(Repositories.JCENTER);
+ }
+
+ /**
+ * Adds the JitPack repository.
+ */
+ public void addJitPack() {
+ addRepository(Repositories.JITPACK);
+ }
+
+ /**
+ * Gets all of the possible download URLs for this library. Entries are
+ * ordered by direct download URLs first and then repository download URLs.
+ *
+ * If the library has a checksum, it will be compared against the
+ * downloaded jar's checksum to verify the integrity of the download. If
+ * the checksums don't match, a warning is generated and the next download
+ * URL is attempted.
+ *
+ * Checksum comparison is ignored if the library doesn't have a checksum
+ * or if the library jar already exists in the save directory.
+ *
+ * Most of the time it is advised to use {@link #loadLibrary(Library)}
+ * instead of this method because this one is only concerned with
+ * downloading the jar and returning the local path. It's usually more
+ * desirable to download the jar and add it to the plugin's classpath in
+ * one operation.
+ *
+ * @param library the library to download
+ * @return local file path to library
+ * @see #loadLibrary(Library)
+ */
+ public Path downloadLibrary(Library library) {
+ Path file = saveDirectory.resolve(requireNonNull(library, "library").getPath());
+ if (Files.exists(file)) {
+ // Early return only if library isn't a snapshot, since snapshot libraries are always re-downloaded
+ if (!library.isSnapshot()) {
+ return file;
+ }
+
+ // Delete the file since the Files.move call down below will fail if it exists
+ try {
+ Files.delete(file);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ Collection
+ * If the provided library has any relocations, they will be applied to
+ * create a relocated jar and the relocated jar will be loaded instead.
+ *
+ * @param library the library to load
+ * @see #downloadLibrary(Library)
+ */
+ public void loadLibrary(Library library) {
+ Path file = downloadLibrary(requireNonNull(library, "library"));
+ if (library.hasRelocations()) {
+ file = relocate(file, library.getRelocatedPath(), library.getRelocations());
+ }
+
+ if (library.isIsolatedLoad()) {
+ addToIsolatedClasspath(library, file);
+ } else {
+ addToClasspath(file);
+ }
+ }
+}
diff --git a/src/main/java/net/byteflux/libby/PaperLibraryManager.java b/src/main/java/net/byteflux/libby/PaperLibraryManager.java
new file mode 100644
index 0000000..af44cac
--- /dev/null
+++ b/src/main/java/net/byteflux/libby/PaperLibraryManager.java
@@ -0,0 +1,85 @@
+package net.byteflux.libby;
+
+import net.byteflux.libby.classloader.URLClassLoaderHelper;
+import net.byteflux.libby.logging.adapters.JDKLogAdapter;
+import org.bukkit.plugin.Plugin;
+
+import java.lang.reflect.Field;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A runtime dependency manager for Paper Plugins. (Not to be confused with bukkit plugins loaded on paper)
+ * See: Paper docs
+ */
+public class PaperLibraryManager extends LibraryManager {
+ /**
+ * Plugin classpath helper
+ */
+ private final URLClassLoaderHelper classLoader;
+
+ /**
+ * Creates a new Paper library manager.
+ *
+ * @param plugin the plugin to manage
+ */
+ public PaperLibraryManager(Plugin plugin) {
+ this(plugin, "lib");
+ }
+
+ /**
+ * Creates a new Paper library manager.
+ *
+ * @param plugin the plugin to manage
+ * @param directoryName download directory name
+ */
+ public PaperLibraryManager(Plugin plugin, String directoryName) {
+ super(new JDKLogAdapter(requireNonNull(plugin, "plugin").getLogger()), plugin.getDataFolder().toPath(), directoryName);
+
+ ClassLoader cl = plugin.getClass().getClassLoader();
+ Class> paperClClazz;
+
+ try {
+ paperClClazz = Class.forName("io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader");
+ } catch (ClassNotFoundException e) {
+ System.err.println("PaperPluginClassLoader not found, are you using Paper 1.19.3+?");
+ throw new RuntimeException(e);
+ }
+
+ if (!paperClClazz.isAssignableFrom(cl.getClass())) {
+ throw new RuntimeException("Plugin classloader is not a PaperPluginClassLoader, are you using paper-plugin.yml?");
+ }
+
+ Field libraryLoaderField;
+
+ try {
+ libraryLoaderField = paperClClazz.getDeclaredField("libraryLoader");
+ } catch (NoSuchFieldException e) {
+ System.err.println("Cannot find libraryLoader field in PaperPluginClassLoader, please open a bug report.");
+ throw new RuntimeException(e);
+ }
+
+ libraryLoaderField.setAccessible(true);
+
+ URLClassLoader libraryLoader;
+ try {
+ libraryLoader = (URLClassLoader) libraryLoaderField.get(cl);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e); // Should never happen
+ }
+
+ classLoader = new URLClassLoaderHelper(libraryLoader, this);
+ }
+
+ /**
+ * Adds a file to the Paper plugin's classpath.
+ *
+ * @param file the file to add
+ */
+ @Override
+ protected void addToClasspath(Path file) {
+ classLoader.addToClasspath(file);
+ }
+}
diff --git a/src/main/java/net/byteflux/libby/Repositories.java b/src/main/java/net/byteflux/libby/Repositories.java
new file mode 100644
index 0000000..2fae350
--- /dev/null
+++ b/src/main/java/net/byteflux/libby/Repositories.java
@@ -0,0 +1,31 @@
+package net.byteflux.libby;
+
+/**
+ * Class containing URLs of public repositories.
+ */
+public class Repositories {
+
+ /**
+ * Maven Central repository URL.
+ */
+ public static final String MAVEN_CENTRAL = "https://repo1.maven.org/maven2/";
+
+ /**
+ * Sonatype OSS repository URL.
+ */
+ public static final String SONATYPE = "https://oss.sonatype.org/content/groups/public/";
+
+ /**
+ * Bintray JCenter repository URL.
+ */
+ public static final String JCENTER = "https://jcenter.bintray.com/";
+
+ /**
+ * JitPack repository URL.
+ */
+ public static final String JITPACK = "https://jitpack.io/";
+
+ private Repositories() {
+ throw new UnsupportedOperationException("Private constructor");
+ }
+}
diff --git a/src/main/java/net/byteflux/libby/classloader/IsolatedClassLoader.java b/src/main/java/net/byteflux/libby/classloader/IsolatedClassLoader.java
new file mode 100644
index 0000000..965d778
--- /dev/null
+++ b/src/main/java/net/byteflux/libby/classloader/IsolatedClassLoader.java
@@ -0,0 +1,51 @@
+package net.byteflux.libby.classloader;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * This class loader is a simple child of {@code URLClassLoader} that uses
+ * the JVM's Extensions Class Loader as the parent instead of the system class
+ * loader to provide an unpolluted classpath.
+ */
+public class IsolatedClassLoader extends URLClassLoader {
+ static {
+ ClassLoader.registerAsParallelCapable();
+ }
+
+ /**
+ * Creates a new isolated class loader for the given URLs.
+ *
+ * @param urls the URLs to add to the classpath
+ */
+ public IsolatedClassLoader(URL... urls) {
+ super(requireNonNull(urls, "urls"), ClassLoader.getSystemClassLoader().getParent());
+ }
+
+ /**
+ * Adds a URL to the classpath.
+ *
+ * @param url the URL to add
+ */
+ @Override
+ public void addURL(URL url) {
+ super.addURL(url);
+ }
+
+ /**
+ * Adds a path to the classpath.
+ *
+ * @param path the path to add
+ */
+ public void addPath(Path path) {
+ try {
+ addURL(requireNonNull(path, "path").toUri().toURL());
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/src/main/java/net/byteflux/libby/classloader/URLClassLoaderHelper.java b/src/main/java/net/byteflux/libby/classloader/URLClassLoaderHelper.java
new file mode 100644
index 0000000..403c2a6
--- /dev/null
+++ b/src/main/java/net/byteflux/libby/classloader/URLClassLoaderHelper.java
@@ -0,0 +1,233 @@
+package net.byteflux.libby.classloader;
+
+import net.byteflux.libby.Library;
+import net.byteflux.libby.LibraryManager;
+import net.byteflux.libby.Repositories;
+import sun.misc.Unsafe;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A reflection-based wrapper around {@link URLClassLoader} for adding URLs to
+ * the classpath.
+ */
+public class URLClassLoaderHelper {
+
+ /**
+ * Unsafe class instance. Used in {@link #getPrivilegedMethodHandle(Method)}.
+ */
+ private static final Unsafe theUnsafe;
+
+ static {
+ Unsafe unsafe = null; // Used to make theUnsafe field final
+
+ // getDeclaredField("theUnsafe") is not used to avoid breakage on JVMs with changed field name
+ for (Field f : Unsafe.class.getDeclaredFields()) {
+ try {
+ if (f.getType() == Unsafe.class && Modifier.isStatic(f.getModifiers())) {
+ f.setAccessible(true);
+ unsafe = (Unsafe) f.get(null);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ theUnsafe = unsafe;
+ }
+
+ /**
+ * The class loader being managed by this helper.
+ */
+ private final URLClassLoader classLoader;
+
+ /**
+ * A reflected method in {@link URLClassLoader}, when invoked adds a URL to the classpath.
+ */
+ private MethodHandle addURLMethodHandle = null;
+
+ /**
+ * Creates a new URL class loader helper.
+ *
+ * @param classLoader the class loader to manage
+ * @param libraryManager the library manager used to download dependencies
+ */
+ public URLClassLoaderHelper(URLClassLoader classLoader, LibraryManager libraryManager) {
+ requireNonNull(libraryManager, "libraryManager");
+ this.classLoader = requireNonNull(classLoader, "classLoader");
+
+ try {
+ Method addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
+
+ try {
+ openUrlClassLoaderModule();
+ } catch (Exception ignored) {
+ }
+
+ try {
+ addURLMethod.setAccessible(true);
+ } catch (Exception exception) {
+ // InaccessibleObjectException has been added in Java 9
+ if (exception.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
+ // It is Java 9+, try to open java.net package
+ if (theUnsafe != null)
+ try {
+ addURLMethodHandle = getPrivilegedMethodHandle(addURLMethod).bindTo(classLoader);
+ return; // We're done
+ } catch (Exception ignored) {
+ addURLMethodHandle = null; // Just to be sure the field is set to null
+ }
+ // Cannot use privileged MethodHandles.Lookup, trying with java agent
+ try {
+ addOpensWithAgent(libraryManager);
+ addURLMethod.setAccessible(true);
+ } catch (Exception e) {
+ // Cannot access at all
+ System.err.println("Cannot access URLClassLoader#addURL(URL), if you are using Java 9+ try to add the following option to your java command: --add-opens java.base/java.net=ALL-UNNAMED");
+ throw new RuntimeException("Cannot access URLClassLoader#addURL(URL)", e);
+ }
+ } else {
+ throw new RuntimeException("Cannot set accessible URLClassLoader#addURL(URL)", exception);
+ }
+ }
+ this.addURLMethodHandle = MethodHandles.lookup().unreflect(addURLMethod).bindTo(classLoader);
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Adds a URL to the class loader's classpath.
+ *
+ * @param url the URL to add
+ */
+ public void addToClasspath(URL url) {
+ try {
+ addURLMethodHandle.invokeWithArguments(requireNonNull(url, "url"));
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Adds a path to the class loader's classpath.
+ *
+ * @param path the path to add
+ */
+ public void addToClasspath(Path path) {
+ try {
+ addToClasspath(requireNonNull(path, "path").toUri().toURL());
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private static void openUrlClassLoaderModule() throws Exception {
+ //
+ // Thanks to lucko (Luck)
+ * Returns true if provided log level is equal to or more severe than the
+ * logger's configured log level.
+ *
+ * @param level the level to check
+ * @return true if message can be logged, or false
+ */
+ private boolean canLog(LogLevel level) {
+ return requireNonNull(level, "level").compareTo(this.level) >= 0;
+ }
+
+ /**
+ * Logs a message with the provided level.
+ *
+ * If the provided log level is less severe than the logger's
+ * configured log level, this message won't be logged.
+ *
+ * @param level message severity level
+ * @param message the message to log
+ * @see #debug(String)
+ * @see #info(String)
+ * @see #warn(String)
+ * @see #error(String)
+ */
+ public void log(LogLevel level, String message) {
+ if (canLog(level)) {
+ adapter.log(level, message);
+ }
+ }
+
+ /**
+ * Logs a message and stack trace with the provided level.
+ *
+ * If the provided log level is less severe than the logger's
+ * configured log level, this message won't be logged.
+ *
+ * @param level message severity level
+ * @param message the message to log
+ * @param throwable the throwable to print
+ * @see #debug(String, Throwable)
+ * @see #info(String, Throwable)
+ * @see #warn(String, Throwable)
+ * @see #error(String, Throwable)
+ */
+ public void log(LogLevel level, String message, Throwable throwable) {
+ if (canLog(level)) {
+ adapter.log(level, message, throwable);
+ }
+ }
+
+ /**
+ * Logs a debug message.
+ *
+ * If the logger's configured log level is more severe than
+ * {@link LogLevel#DEBUG}, this message won't be logged.
+ *
+ * @param message the message to log
+ */
+ public void debug(String message) {
+ log(LogLevel.DEBUG, message);
+ }
+
+ /**
+ * Logs a debug message with a stack trace.
+ *
+ * If the logger's configured log level is more severe than
+ * {@link LogLevel#DEBUG}, this message won't be logged.
+ *
+ * @param message the message to log
+ * @param throwable the throwable to print
+ */
+ public void debug(String message, Throwable throwable) {
+ log(LogLevel.DEBUG, message, throwable);
+ }
+
+ /**
+ * Logs an informational message.
+ *
+ * If the logger's configured log level is more severe than
+ * {@link LogLevel#INFO}, this message won't be logged.
+ *
+ * @param message the message to log
+ */
+ public void info(String message) {
+ log(LogLevel.INFO, message);
+ }
+
+ /**
+ * Logs an informational message with a stack trace.
+ *
+ * If the logger's configured log level is more severe than
+ * {@link LogLevel#INFO}, this message won't be logged.
+ *
+ * @param message the message to log
+ * @param throwable the throwable to print
+ */
+ public void info(String message, Throwable throwable) {
+ log(LogLevel.INFO, message, throwable);
+ }
+
+ /**
+ * Logs a warning message.
+ *
+ * If the logger's configured log level is more severe than
+ * {@link LogLevel#WARN}, this message won't be logged.
+ *
+ * @param message the message to log
+ */
+ public void warn(String message) {
+ log(LogLevel.WARN, message);
+ }
+
+ /**
+ * Logs a warning message with a stack trace.
+ *
+ * If the logger's configured log level is more severe than
+ * {@link LogLevel#WARN}, this message won't be logged.
+ *
+ * @param message the message to log
+ * @param throwable the throwable to print
+ */
+ public void warn(String message, Throwable throwable) {
+ log(LogLevel.WARN, message, throwable);
+ }
+
+ /**
+ * Logs an error message.
+ *
+ * @param message the message to log
+ */
+ public void error(String message) {
+ log(LogLevel.ERROR, message);
+ }
+
+ /**
+ * Logs an error message with a stack trace.
+ *
+ * @param message message to log
+ * @param throwable the throwable to print
+ */
+ public void error(String message, Throwable throwable) {
+ log(LogLevel.ERROR, message, throwable);
+ }
+}
diff --git a/src/main/java/net/byteflux/libby/logging/adapters/JDKLogAdapter.java b/src/main/java/net/byteflux/libby/logging/adapters/JDKLogAdapter.java
new file mode 100644
index 0000000..b3d0a55
--- /dev/null
+++ b/src/main/java/net/byteflux/libby/logging/adapters/JDKLogAdapter.java
@@ -0,0 +1,77 @@
+package net.byteflux.libby.logging.adapters;
+
+import net.byteflux.libby.logging.LogLevel;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Logging adapter that logs to a JDK logger.
+ */
+public class JDKLogAdapter implements LogAdapter {
+ /**
+ * JDK logger
+ */
+ private final Logger logger;
+
+ /**
+ * Creates a new JDK log adapter that logs to a {@link Logger}.
+ *
+ * @param logger the JDK logger to wrap
+ */
+ public JDKLogAdapter(Logger logger) {
+ this.logger = requireNonNull(logger, "logger");
+ }
+
+ /**
+ * Logs a message with the provided level to the JDK logger.
+ *
+ * @param level message severity level
+ * @param message the message to log
+ */
+ @Override
+ public void log(LogLevel level, String message) {
+ switch (requireNonNull(level, "level")) {
+ case DEBUG:
+ logger.log(Level.FINE, message);
+ break;
+ case INFO:
+ logger.log(Level.INFO, message);
+ break;
+ case WARN:
+ logger.log(Level.WARNING, message);
+ break;
+ case ERROR:
+ logger.log(Level.SEVERE, message);
+ break;
+ }
+ }
+
+ /**
+ * Logs a message and stack trace with the provided level to the JDK
+ * logger.
+ *
+ * @param level message severity level
+ * @param message the message to log
+ * @param throwable the throwable to print
+ */
+ @Override
+ public void log(LogLevel level, String message, Throwable throwable) {
+ switch (requireNonNull(level, "level")) {
+ case DEBUG:
+ logger.log(Level.FINE, message, throwable);
+ break;
+ case INFO:
+ logger.log(Level.INFO, message, throwable);
+ break;
+ case WARN:
+ logger.log(Level.WARNING, message, throwable);
+ break;
+ case ERROR:
+ logger.log(Level.SEVERE, message, throwable);
+ break;
+ }
+ }
+}
diff --git a/src/main/java/net/byteflux/libby/logging/adapters/LogAdapter.java b/src/main/java/net/byteflux/libby/logging/adapters/LogAdapter.java
new file mode 100644
index 0000000..93bfa14
--- /dev/null
+++ b/src/main/java/net/byteflux/libby/logging/adapters/LogAdapter.java
@@ -0,0 +1,25 @@
+package net.byteflux.libby.logging.adapters;
+
+import net.byteflux.libby.logging.LogLevel;
+
+/**
+ * Logging interface for adapting platform-specific loggers to our logging API.
+ */
+public interface LogAdapter {
+ /**
+ * Logs a message with the provided level.
+ *
+ * @param level message severity level
+ * @param message the message to log
+ */
+ void log(LogLevel level, String message);
+
+ /**
+ * Logs a message and stack trace with the provided level.
+ *
+ * @param level message severity level
+ * @param message the message to log
+ * @param throwable the throwable to print
+ */
+ void log(LogLevel level, String message, Throwable throwable);
+}
diff --git a/src/main/java/net/byteflux/libby/relocation/Relocation.java b/src/main/java/net/byteflux/libby/relocation/Relocation.java
new file mode 100644
index 0000000..5c3f8df
--- /dev/null
+++ b/src/main/java/net/byteflux/libby/relocation/Relocation.java
@@ -0,0 +1,184 @@
+package net.byteflux.libby.relocation;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Relocations are used to describe a search and replace pattern for renaming
+ * packages in a library jar for the purpose of preventing namespace conflicts
+ * with other plugins that bundle their own version of the same library.
+ */
+public class Relocation {
+ /**
+ * Search pattern
+ */
+ private final String pattern;
+
+ /**
+ * Replacement pattern
+ */
+ private final String relocatedPattern;
+
+ /**
+ * Classes and resources to include
+ */
+ private final Collection
This method also resolves SNAPSHOT artifacts URLs.
+ *
+ * @param library the library to resolve
+ * @return download URLs
+ */
+ public Collection