This commit is contained in:
YuTian 2024-08-08 17:09:59 +08:00
parent dde365448e
commit 706de6f042
20 changed files with 2331 additions and 4 deletions

View File

@ -113,28 +113,33 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<version>1.5.6-3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.0.0</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.3</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -3,21 +3,47 @@ package com.io.yutian.elementoriginlib;
import com.io.yutian.elementoriginlib.lang.Lang;
import com.io.yutian.elementoriginlib.listener.GuiHandlerListener;
import com.io.yutian.elementoriginlib.listener.PlayerChatInputListener;
import com.io.yutian.elementoriginlib.logger.Logger;
import com.io.yutian.elementoriginlib.redis.RedisIO;
import net.byteflux.libby.*;
import net.byteflux.libby.logging.LogLevel;
import org.bukkit.plugin.java.JavaPlugin;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
public final class ElementOriginLib extends JavaPlugin {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(ElementOriginLib.class);
private static ElementOriginLib instance;
private static Logger logger = Logger.getLogger(ElementOriginLib.class);
@Override
public void onEnable() {
instance = this;
loadLibraries();
Lang.registerLangFile(this);
Lang.reload();
new GuiHandlerListener(this);
new PlayerChatInputListener(this);
logger.info("Successfully load ElementOriginLib v"+getPluginMeta().getVersion());
}
private void loadLibraries() {
logger.info("Loading libraries...");
LibraryManager libraryManager = new BukkitLibraryManager(this);
libraryManager.addMavenCentral();
libraryManager.setLogLevel(LogLevel.WARN);
libraryManager.loadLibrary(Library.builder().groupId("org{}apache{}commons").artifactId("commons-compress").version("1.26.2").build());
libraryManager.loadLibrary(Library.builder().groupId("com{}github{}luben").artifactId("zstd-jni").version("1.5.6-3").build());
libraryManager.loadLibrary(Library.builder().groupId("com{}zaxxer").artifactId("HikariCP").version("4.0.3").build());
libraryManager.loadLibrary(Library.builder().groupId("org{}xerial").artifactId("sqlite-jdbc").version("3.46.0.0").build());
libraryManager.loadLibrary(Library.builder().groupId("redis{}clients").id("jedis").artifactId("jedis").version("5.1.3").build());
logger.info("Successfully loaded libraries.");
}
@Override

View File

@ -0,0 +1,64 @@
package com.io.yutian.elementoriginlib.logger;
import org.bukkit.Bukkit;
import java.util.logging.Level;
public class Logger {
private Class clazz;
protected String formatClassName;
private Logger(Class clazz) {
this.clazz = clazz;
this.formatClassName = getFormatClassName(clazz, 2);
}
public String getFormatClassName() {
return formatClassName;
}
protected String getFormatClassName(Class clazz, int keepChars) {
String className = clazz.getName();
String[] parts = className.split("\\.");
StringBuilder abbreviated = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (i < parts.length - 1) {
abbreviated.append(parts[i].substring(0, Math.min(keepChars, parts[i].length()))).append(".");
} else {
abbreviated.append(parts[i]);
}
}
return abbreviated.toString();
}
private String getPrefix(String type) {
return "[" + clazz.getSimpleName() +"] ";
}
public void info(String msg) {
Bukkit.getLogger().info(getPrefix("INFO")+ msg);
}
public void warn(String msg) {
Bukkit.getLogger().warning(getPrefix("WARN") + msg);
}
public void error(String msg) {
Bukkit.getLogger().log(Level.SEVERE, getPrefix("ERROR") + msg);
}
public void fatal(String msg) {
Bukkit.getLogger().log(Level.SEVERE, "[!!!] "+getPrefix("FATAL") + msg);
}
public void debug(String msg) {
Bukkit.getLogger().finer(getPrefix("DEBUG") + msg);
}
public static Logger getLogger(Class clazz) {
return new Logger(clazz);
}
}

View File

@ -1,7 +1,7 @@
package com.io.yutian.elementoriginlib.nbt;
import net.minecraft.nbt.NBTTagCompound;
import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftItemStack;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;

View File

@ -4,7 +4,7 @@ import com.io.yutian.elementoriginlib.exception.SerializeException;
import com.io.yutian.elementoriginlib.serialize.serializers.ItemStackSerializer;
import com.io.yutian.elementoriginlib.serialize.serializers.UUIDSerializer;
import com.io.yutian.elementoriginlib.util.ReflectionUtil;
import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftItemStack;
import org.bukkit.inventory.ItemStack;
import org.json.JSONArray;
import org.json.JSONObject;

View File

@ -3,7 +3,7 @@ package com.io.yutian.elementoriginlib.serialize.serializers;
import com.io.yutian.elementoriginlib.serialize.Serializer;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.nbt.*;
import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftItemStack;
import org.bukkit.inventory.ItemStack;
import org.json.JSONArray;
import org.json.JSONObject;
@ -52,7 +52,7 @@ public class ItemStackSerializer implements Serializer<ItemStack> {
return tagByteArray.e();
case 8:
NBTTagString nbtTagString = (NBTTagString) nbtBase;
return nbtTagString.m_();
return nbtTagString.t_();
case 9:
NBTTagList nbtTagList = (NBTTagList) nbtBase;
JSONArray jsonArray = new JSONArray();

View File

@ -0,0 +1,50 @@
package net.byteflux.libby;
import net.byteflux.libby.classloader.URLClassLoaderHelper;
import net.byteflux.libby.logging.adapters.JDKLogAdapter;
import org.bukkit.plugin.Plugin;
import java.net.URLClassLoader;
import java.nio.file.Path;
import static java.util.Objects.requireNonNull;
/**
* A runtime dependency manager for Bukkit plugins.
*/
public class BukkitLibraryManager extends LibraryManager {
/**
* Plugin classpath helper
*/
private final URLClassLoaderHelper classLoader;
/**
* Creates a new Bukkit library manager.
*
* @param plugin the plugin to manage
*/
public BukkitLibraryManager(Plugin plugin) {
this(plugin, "lib");
}
/**
* Creates a new Bukkit library manager.
*
* @param plugin the plugin to manage
* @param directoryName download directory name
*/
public BukkitLibraryManager(Plugin plugin, String directoryName) {
super(new JDKLogAdapter(requireNonNull(plugin, "plugin").getLogger()), plugin.getDataFolder().toPath(), directoryName);
classLoader = new URLClassLoaderHelper((URLClassLoader) plugin.getClass().getClassLoader(), this);
}
/**
* Adds a file to the Bukkit plugin's classpath.
*
* @param file the file to add
*/
@Override
protected void addToClasspath(Path file) {
classLoader.addToClasspath(file);
}
}

View File

@ -0,0 +1,16 @@
package net.byteflux.libby;
/**
* Filtered Maven properties and other related constants.
*/
public class LibbyProperties {
/**
* Project version
*/
public static final String VERSION = "${project.version}";
/**
* User agent string to use when downloading libraries
*/
public static final String HTTP_USER_AGENT = "libby/" + VERSION;
}

View File

@ -0,0 +1,533 @@
package net.byteflux.libby;
import net.byteflux.libby.relocation.Relocation;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.UUID;
import static java.util.Objects.requireNonNull;
/**
* An immutable representation of a Maven artifact that can be downloaded,
* relocated and then loaded into a plugin's classpath at runtime.
*
* @see #builder()
*/
public class Library {
/**
* Direct download URLs for this library
*/
private final Collection<String> urls;
/**
* Repository URLs for this library
*/
private final Collection<String> repositories;
/**
* Library id (used by Isolated Class Loaders)
*/
private final String id;
/**
* Maven group ID
*/
private final String groupId;
/**
* Maven artifact ID
*/
private final String artifactId;
/**
* Artifact version
*/
private final String version;
/**
* Artifact classifier
*/
private final String classifier;
/**
* Binary SHA-256 checksum for this library's jar file
*/
private final byte[] checksum;
/**
* Jar relocations to apply
*/
private final Collection<Relocation> relocations;
/**
* Relative Maven path to this library's artifact
*/
private final String path;
/**
* Relative partial Maven path to this library
*/
private final String partialPath;
/**
* Relative path to this library's relocated jar
*/
private final String relocatedPath;
/**
* Should this library be loaded in an isolated class loader?
*/
private final boolean isolatedLoad;
/**
* Creates a new library.
*
* @param urls direct download URLs
* @param id library ID
* @param groupId Maven group ID
* @param artifactId Maven artifact ID
* @param version artifact version
* @param classifier artifact classifier or null
* @param checksum binary SHA-256 checksum or null
* @param relocations jar relocations or null
* @param isolatedLoad isolated load for this library
*/
private Library(Collection<String> urls,
String id,
String groupId,
String artifactId,
String version,
String classifier,
byte[] checksum,
Collection<Relocation> relocations,
boolean isolatedLoad) {
this(urls, null, id, groupId, artifactId, version, classifier, checksum, relocations, isolatedLoad);
}
/**
* Creates a new library.
*
* @param urls direct download URLs
* @param repositories repository URLs
* @param id library ID
* @param groupId Maven group ID
* @param artifactId Maven artifact ID
* @param version artifact version
* @param classifier artifact classifier or null
* @param checksum binary SHA-256 checksum or null
* @param relocations jar relocations or null
* @param isolatedLoad isolated load for this library
*/
private Library(Collection<String> urls,
Collection<String> repositories,
String id,
String groupId,
String artifactId,
String version,
String classifier,
byte[] checksum,
Collection<Relocation> relocations,
boolean isolatedLoad) {
this.urls = urls != null ? Collections.unmodifiableList(new LinkedList<>(urls)) : Collections.emptyList();
this.id = id != null ? id : UUID.randomUUID().toString();
this.groupId = requireNonNull(groupId, "groupId").replace("{}", ".");
this.artifactId = requireNonNull(artifactId, "artifactId");
this.version = requireNonNull(version, "version");
this.classifier = classifier;
this.checksum = checksum;
this.relocations = relocations != null ? Collections.unmodifiableList(new LinkedList<>(relocations)) : Collections.emptyList();
this.partialPath = this.groupId.replace('.', '/') + '/' + artifactId + '/' + version + '/';
String path = this.partialPath + artifactId + '-' + version;
if (hasClassifier()) {
path += '-' + classifier;
}
this.path = path + ".jar";
this.repositories = repositories != null ? Collections.unmodifiableList(new LinkedList<>(repositories)) : Collections.emptyList();
relocatedPath = hasRelocations() ? path + "-relocated.jar" : null;
this.isolatedLoad = isolatedLoad;
}
/**
* Gets the direct download URLs for this library.
*
* @return direct download URLs
*/
public Collection<String> getUrls() {
return urls;
}
/**
* Gets the repositories URLs for this library.
*
* @return repositories URLs
*/
public Collection<String> getRepositories() {
return repositories;
}
/**
* Gets the library ID
*
* @return the library id
*/
public String getId() {
return id;
}
/**
* Gets the Maven group ID for this library.
*
* @return Maven group ID
*/
public String getGroupId() {
return groupId;
}
/**
* Gets the Maven artifact ID for this library.
*
* @return Maven artifact ID
*/
public String getArtifactId() {
return artifactId;
}
/**
* Gets the artifact version for this library.
*
* @return artifact version
*/
public String getVersion() {
return version;
}
/**
* Gets the artifact classifier for this library.
*
* @return artifact classifier or null
*/
public String getClassifier() {
return classifier;
}
/**
* Gets whether this library has an artifact classifier.
*
* @return true if library has classifier, false otherwise
*/
public boolean hasClassifier() {
return classifier != null;
}
/**
* Gets the binary SHA-256 checksum of this library's jar file.
*
* @return checksum or null
*/
public byte[] getChecksum() {
return checksum;
}
/**
* Gets whether this library has a checksum.
*
* @return true if library has checksum, false otherwise
*/
public boolean hasChecksum() {
return checksum != null;
}
/**
* Gets the jar relocations to apply to this library.
*
* @return jar relocations to apply
*/
public Collection<Relocation> getRelocations() {
return relocations;
}
/**
* Gets whether this library has any jar relocations.
*
* @return true if library has relocations, false otherwise
*/
public boolean hasRelocations() {
return !relocations.isEmpty();
}
/**
* Gets the relative Maven path to this library's artifact.
*
* @return relative Maven path for this library
*/
public String getPath() {
return path;
}
/**
* Gets the relative partial Maven path to this library.
*
* @return relative partial Maven path for this library
*/
public String getPartialPath() {
return partialPath;
}
/**
* Gets the relative path to this library's relocated jar.
*
* @return path to relocated artifact or null if has no relocations
*/
public String getRelocatedPath() {
return relocatedPath;
}
/**
* Is the library loaded isolated?
*
* @return true if the library is loaded isolated
*/
public boolean isIsolatedLoad() {
return isolatedLoad;
}
/**
* Whether the library is a snapshot.
*
* @return whether the library is a snapshot.
*/
public boolean isSnapshot() {
return version.endsWith("-SNAPSHOT");
}
/**
* Gets a concise, human-readable string representation of this library.
*
* @return string representation
*/
@Override
public String toString() {
String name = groupId + ':' + artifactId + ':' + version;
if (hasClassifier()) {
name += ':' + classifier;
}
return name;
}
/**
* Creates a new library builder.
*
* @return new library builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Due to the constructor complexity of an immutable {@link Library},
* instead this fluent builder is used to configure and then construct
* a new library.
*/
public static class Builder {
/**
* Direct download URLs for this library
*/
private final Collection<String> urls = new LinkedList<>();
/**
* Repository URLs for this library
*/
private final Collection<String> repositories = new LinkedList<>();
/**
* The library ID
*/
private String id;
/**
* Maven group ID
*/
private String groupId;
/**
* Maven artifact ID
*/
private String artifactId;
/**
* Artifact version
*/
private String version;
/**
* Artifact classifier
*/
private String classifier;
/**
* Binary SHA-256 checksum for this library's jar file
*/
private byte[] checksum;
/**
* Isolated load
*/
private boolean isolatedLoad;
/**
* Jar relocations to apply
*/
private final Collection<Relocation> relocations = new LinkedList<>();
/**
* Adds a direct download URL for this library.
*
* @param url direct download URL
* @return this builder
*/
public Builder url(String url) {
urls.add(requireNonNull(url, "url"));
return this;
}
/**
* Adds a repository URL for this library.
* <p>Most common repositories can be found in {@link Repositories} class as constants.
* <p>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);
}
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* Transitive dependencies for a library aren't downloaded automatically and
* must be explicitly loaded like every other library.
* <p>
* 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<String> repositories = new LinkedHashSet<>();
/**
* Lazily-initialized relocation helper that uses reflection to call into
* Luck's Jar Relocator
*/
private RelocationHelper relocator;
/**
* Map of isolated class loaders and theirs id
*/
private final Map<String, IsolatedClassLoader> isolatedLibraries = new HashMap<>();
/**
* Creates a new library manager.
*
* @param logAdapter plugin logging adapter
* @param dataDirectory plugin's data directory
*
* @deprecated Use {@link LibraryManager#LibraryManager(LogAdapter, Path, String)}
*/
@Deprecated
protected LibraryManager(LogAdapter logAdapter, Path dataDirectory) {
logger = new Logger(requireNonNull(logAdapter, "logAdapter"));
saveDirectory = requireNonNull(dataDirectory, "dataDirectory").toAbsolutePath().resolve("lib");
}
/**
* Creates a new library manager.
*
* @param logAdapter plugin logging adapter
* @param dataDirectory plugin's data directory
* @param directoryName download directory name
*/
protected LibraryManager(LogAdapter logAdapter, Path dataDirectory, String directoryName) {
logger = new Logger(requireNonNull(logAdapter, "logAdapter"));
saveDirectory = requireNonNull(dataDirectory, "dataDirectory").toAbsolutePath().resolve(requireNonNull(directoryName, "directoryName"));
}
/**
* Adds a file to the plugin's classpath.
*
* @param file the file to add
*/
protected abstract void addToClasspath(Path file);
/**
* Adds a file to the isolated class loader
*
* @param library the library to add
* @param file the file to add
*/
protected void addToIsolatedClasspath(Library library, Path file) {
IsolatedClassLoader classLoader;
String id = library.getId();
if (id != null) {
classLoader = isolatedLibraries.computeIfAbsent(id, s -> new IsolatedClassLoader());
} else {
classLoader = new IsolatedClassLoader();
}
classLoader.addPath(file);
}
/**
* Get the isolated class loader of the library
*
* @param libraryId the id of the library
*/
public IsolatedClassLoader getIsolatedClassLoaderOf(String libraryId) {
return isolatedLibraries.get(libraryId);
}
/**
* Gets the logging level for this library manager.
*
* @return log level
*/
public LogLevel getLogLevel() {
return logger.getLevel();
}
/**
* Sets the logging level for this library manager.
* <p>
* 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.
* <p>
* 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.
* <p>
* For each library this list is traversed to download artifacts after the
* direct download URLs have been attempted.
*
* @return current repositories
*/
public Collection<String> getRepositories() {
List<String> urls;
synchronized (repositories) {
urls = new LinkedList<>(repositories);
}
return Collections.unmodifiableList(urls);
}
/**
* Adds a repository URL to this library manager.
* <p>
* 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.
* <br>This method also resolves SNAPSHOT artifacts URLs.
*
* @param library the library to resolve
* @return download URLs
*/
public Collection<String> resolveLibrary(Library library) {
Set<String> urls = new LinkedHashSet<>(requireNonNull(library, "library").getUrls());
boolean snapshot = library.isSnapshot();
// Try from library-declared repos first
for (String repository : library.getRepositories()) {
if (snapshot) {
String url = resolveSnapshot(repository, library);
if (url != null)
urls.add(repository + url);
} else {
urls.add(repository + library.getPath());
}
}
for (String repository : getRepositories()) {
if (snapshot) {
String url = resolveSnapshot(repository, library);
if (url != null)
urls.add(repository + url);
} else {
urls.add(repository + library.getPath());
}
}
return Collections.unmodifiableSet(urls);
}
/**
* Resolves the URL of the artifact of a snapshot library.
*
* @param repository The repository to query for snapshot information
* @param library The library
* @return The URl of the artifact of a snapshot library or null if no information could be gathered from the
* provided repository
*/
private String resolveSnapshot(String repository, Library library) {
String url = requireNonNull(repository, "repository") + requireNonNull(library, "library").getPartialPath() + "maven-metadata.xml";
try {
URLConnection connection = new URL(requireNonNull(url, "url")).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", LibbyProperties.HTTP_USER_AGENT);
try (InputStream in = connection.getInputStream()) {
return getURLFromMetadata(in, library);
}
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
if (e instanceof FileNotFoundException) {
logger.debug("File not found: " + url);
} else if (e instanceof SocketTimeoutException) {
logger.debug("Connect timed out: " + url);
} else if (e instanceof UnknownHostException) {
logger.debug("Unknown host: " + url);
} else {
logger.debug("Unexpected IOException", e);
}
return null;
}
}
/**
* Gets the URL of the artifact of a snapshot library from the provided InputStream, which should be opened to the
* library's maven-metadata.xml.
*
* @param inputStream The InputStream opened to the library's maven-metadata.xml
* @param library The library
* @return The URl of the artifact of a snapshot library or null if no information could be gathered from the
* provided inputStream
* @throws IOException If any IO errors occur
*/
private String getURLFromMetadata(InputStream inputStream, Library library) throws IOException {
requireNonNull(inputStream, "inputStream");
requireNonNull(library, "library");
String timestamp, buildNumber;
try {
// This reads the maven-metadata.xml file and gets the snapshot info from the <snapshot> tag.
// Example tag:
// <snapshot>
// <timestamp>20220617.013635</timestamp>
// <buildNumber>12</buildNumber>
// </snapshot>
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(inputStream);
doc.getDocumentElement().normalize();
NodeList nodes = doc.getElementsByTagName("snapshot");
if (nodes.getLength() == 0) {
return null;
}
Node snapshot = nodes.item(0);
if (snapshot.getNodeType() != Node.ELEMENT_NODE) {
return null;
}
Node timestampNode = ((Element) snapshot).getElementsByTagName("timestamp").item(0);
if (timestampNode == null || timestampNode.getNodeType() != Node.ELEMENT_NODE) {
return null;
}
Node buildNumberNode = ((Element) snapshot).getElementsByTagName("buildNumber").item(0);
if (buildNumberNode == null || buildNumberNode.getNodeType() != Node.ELEMENT_NODE) {
return null;
}
Node timestampChild = timestampNode.getFirstChild();
if (timestampChild == null || timestampChild.getNodeType() != Node.TEXT_NODE) {
return null;
}
Node buildNumberChild = buildNumberNode.getFirstChild();
if (buildNumberChild == null || buildNumberChild.getNodeType() != Node.TEXT_NODE) {
return null;
}
timestamp = timestampChild.getNodeValue();
buildNumber = buildNumberChild.getNodeValue();
} catch (ParserConfigurationException | SAXException e) {
logger.debug("Invalid maven-metadata.xml", e);
return null;
}
String version = library.getVersion();
// Call .substring(...) only on versions ending in "-SNAPSHOT".
// It should never happen that a snapshot version doesn't end in "-SNAPSHOT", but better be sure
if (version.endsWith("-SNAPSHOT")) {
version = version.substring(0, version.length() - "-SNAPSHOT".length());
}
String url = library.getPartialPath() + library.getArtifactId() + '-' + version + '-' + timestamp + '-' + buildNumber;
if (library.hasClassifier()) {
url += '-' + library.getClassifier();
}
return url + ".jar";
}
/**
* Downloads a library jar and returns the contents as a byte array.
*
* @param url the URL to the library jar
* @return downloaded jar as byte array or null if nothing was downloaded
*/
private byte[] downloadLibrary(String url) {
try {
URLConnection connection = new URL(requireNonNull(url, "url")).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", LibbyProperties.HTTP_USER_AGENT);
try (InputStream in = connection.getInputStream()) {
int len;
byte[] buf = new byte[8192];
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
} catch (SocketTimeoutException e) {
logger.warn("Download timed out: " + connection.getURL());
return null;
}
logger.info("Downloaded library " + connection.getURL());
return out.toByteArray();
}
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
if (e instanceof FileNotFoundException) {
logger.debug("File not found: " + url);
} else if (e instanceof SocketTimeoutException) {
logger.debug("Connect timed out: " + url);
} else if (e instanceof UnknownHostException) {
logger.debug("Unknown host: " + url);
} else {
logger.debug("Unexpected IOException", e);
}
return null;
}
}
/**
* Downloads a library jar to the save directory if it doesn't already
* exist (snapshot libraries are always re-downloaded) and returns
* the local file path.
* <p>
* 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.
* <p>
* Checksum comparison is ignored if the library doesn't have a checksum
* or if the library jar already exists in the save directory.
* <p>
* 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<String> urls = resolveLibrary(library);
if (urls.isEmpty()) {
throw new RuntimeException("Library '" + library + "' couldn't be resolved, add a repository");
}
MessageDigest md = null;
if (library.hasChecksum()) {
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
Path out = file.resolveSibling(file.getFileName() + ".tmp");
out.toFile().deleteOnExit();
try {
Files.createDirectories(file.getParent());
for (String url : urls) {
byte[] bytes = downloadLibrary(url);
if (bytes == null) {
continue;
}
if (md != null) {
byte[] checksum = md.digest(bytes);
if (!Arrays.equals(checksum, library.getChecksum())) {
logger.warn("*** INVALID CHECKSUM ***");
logger.warn(" Library : " + library);
logger.warn(" URL : " + url);
logger.warn(" Expected : " + Base64.getEncoder().encodeToString(library.getChecksum()));
logger.warn(" Actual : " + Base64.getEncoder().encodeToString(checksum));
continue;
}
}
Files.write(out, bytes);
Files.move(out, file);
return file;
}
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
try {
Files.deleteIfExists(out);
} catch (IOException ignored) {
}
}
throw new RuntimeException("Failed to download library '" + library + "'");
}
/**
* Processes the input jar and generates an output jar with the provided
* relocation rules applied, then returns the path to the relocated jar.
*
* @param in input jar
* @param out output jar
* @param relocations relocations to apply
* @return the relocated file
* @see RelocationHelper#relocate(Path, Path, Collection)
*/
private Path relocate(Path in, String out, Collection<Relocation> relocations) {
requireNonNull(in, "in");
requireNonNull(out, "out");
requireNonNull(relocations, "relocations");
Path file = saveDirectory.resolve(out);
if (Files.exists(file)) {
return file;
}
Path tmpOut = file.resolveSibling(file.getFileName() + ".tmp");
tmpOut.toFile().deleteOnExit();
synchronized (this) {
if (relocator == null) {
relocator = new RelocationHelper(this);
}
}
try {
relocator.relocate(in, tmpOut, relocations);
Files.move(tmpOut, file);
logger.info("Relocations applied to " + saveDirectory.getParent().relativize(in));
return file;
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
try {
Files.deleteIfExists(tmpOut);
} catch (IOException ignored) {
}
}
}
/**
* Loads a library jar into the plugin's classpath. If the library jar
* doesn't exist locally, it will be downloaded.
* <p>
* 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);
}
}
}

View File

@ -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: <a href="https://docs.papermc.io/paper/dev/getting-started/paper-plugins">Paper docs</a>
*/
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);
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}
}

View File

@ -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) <luck@lucko.me> for this snippet used in his own class loader
//
// This is a workaround used to maintain Java 9+ support with reflections
// Thanks to this you will be able to run this class loader with Java 8+
// This is effectively calling:
//
// URLClassLoader.class.getModule().addOpens(
// URLClassLoader.class.getPackageName(),
// URLClassLoaderHelper.class.getModule()
// );
//
// We use reflection since we build against Java 8.
Class<?> moduleClass = Class.forName("java.lang.Module");
Method getModuleMethod = Class.class.getMethod("getModule");
Method addOpensMethod = moduleClass.getMethod("addOpens", String.class, moduleClass);
Object urlClassLoaderModule = getModuleMethod.invoke(URLClassLoader.class);
Object thisModule = getModuleMethod.invoke(URLClassLoaderHelper.class);
addOpensMethod.invoke(urlClassLoaderModule, URLClassLoader.class.getPackage().getName(), thisModule);
}
private MethodHandle getPrivilegedMethodHandle(Method method) throws Exception {
// Try to get a MethodHandle to URLClassLoader#addURL.
// The Unsafe class is used to get a privileged MethodHandles.Lookup instance.
// Looking for MethodHandles.Lookup#IMPL_LOOKUP private static field
// getDeclaredField("IMPL_LOOKUP") is not used to avoid breakage on JVMs with changed field name
for (Field trustedLookup : MethodHandles.Lookup.class.getDeclaredFields()) {
if (trustedLookup.getType() != MethodHandles.Lookup.class || !Modifier.isStatic(trustedLookup.getModifiers()) || trustedLookup.isSynthetic())
continue;
try {
MethodHandles.Lookup lookup = (MethodHandles.Lookup) theUnsafe.getObject(theUnsafe.staticFieldBase(trustedLookup), theUnsafe.staticFieldOffset(trustedLookup));
return lookup.unreflect(method);
} catch (Exception ignored) {
// Unreflect went wrong, trying the next field
}
}
// Every field has been tried
throw new RuntimeException("Cannot get privileged method handle.");
}
private void addOpensWithAgent(LibraryManager libraryManager) throws Exception {
// To open URLClassLoader's module we need permissions.
// Try to add a java agent at runtime (specifically, ByteBuddy's agent) and use it to open the module,
// since java agents should have such permission.
// Download ByteBuddy's agent and load it through an IsolatedClassLoader
IsolatedClassLoader isolatedClassLoader = new IsolatedClassLoader();
try {
isolatedClassLoader.addPath(libraryManager.downloadLibrary(
Library.builder()
.groupId("net.bytebuddy")
.artifactId("byte-buddy-agent")
.version("1.12.1")
.checksum("mcCtBT9cljUEniB5ESpPDYZMfVxEs1JRPllOiWTP+bM=")
.repository(Repositories.MAVEN_CENTRAL)
.build()
));
Class<?> byteBuddyAgent = isolatedClassLoader.loadClass("net.bytebuddy.agent.ByteBuddyAgent");
// This is effectively calling:
//
// Instrumentation instrumentation = ByteBuddyAgent.install();
// instrumentation.redefineModule(
// URLClassLoader.class.getModule(),
// Collections.emptySet(),
// Collections.emptyMap(),
// Collections.singletonMap("java.net", Collections.singleton(getClass().getModule())),
// Collections.emptySet(),
// Collections.emptyMap()
// );
//
// For more information see https://docs.oracle.com/en/java/javase/16/docs/api/java.instrument/java/lang/instrument/Instrumentation.html
//
// We use reflection since we build against Java 8.
Object instrumentation = byteBuddyAgent.getDeclaredMethod("install").invoke(null);
Class<?> instrumentationClass = Class.forName("java.lang.instrument.Instrumentation");
Method redefineModule = instrumentationClass.getDeclaredMethod("redefineModule", Class.forName("java.lang.Module"), Set.class, Map.class, Map.class, Set.class, Map.class);
Method getModule = Class.class.getDeclaredMethod("getModule");
Map<String, Set<?>> toOpen = Collections.singletonMap("java.net", Collections.singleton(getModule.invoke(getClass())));
redefineModule.invoke(instrumentation, getModule.invoke(URLClassLoader.class), Collections.emptySet(), Collections.emptyMap(), toOpen, Collections.emptySet(), Collections.emptyMap());
} finally {
try {
isolatedClassLoader.close();
} catch (Exception ignored) {
}
}
}
}

View File

@ -0,0 +1,26 @@
package net.byteflux.libby.logging;
/**
* Represents the severity of a log message in {@link Logger}.
*/
public enum LogLevel {
/**
* Stuff that isn't useful to end-users
*/
DEBUG,
/**
* Stuff that might be useful to know
*/
INFO,
/**
* Non-fatal, often recoverable errors or notices
*/
WARN,
/**
* Probably an unrecoverable error
*/
ERROR
}

View File

@ -0,0 +1,195 @@
package net.byteflux.libby.logging;
import net.byteflux.libby.logging.adapters.LogAdapter;
import static java.util.Objects.requireNonNull;
/**
* A logging wrapper that logs to a log adapter and can be configured to filter
* log messages by severity.
*/
public class Logger {
/**
* Log adapter for the current platform
*/
private final LogAdapter adapter;
/**
* Log level controlling which messages are logged
*/
private LogLevel level = LogLevel.INFO;
/**
* Creates a new logger with the provided adapter.
*
* @param adapter the adapter to wrap
*/
public Logger(LogAdapter adapter) {
this.adapter = requireNonNull(adapter, "adapter");
}
/**
* Gets the current log level.
*
* @return current log level
*/
public LogLevel getLevel() {
return level;
}
/**
* Sets a new log level.
*
* @param level new log level
*/
public void setLevel(LogLevel level) {
this.level = requireNonNull(level, "level");
}
/**
* Gets whether messages matching the provided level can be logged under
* the current log level setting.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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);
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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<String> includes;
/**
* Classes and resources to exclude
*/
private final Collection<String> excludes;
/**
* Creates a new relocation.
*
* @param pattern search pattern
* @param relocatedPattern replacement pattern
* @param includes classes and resources to include
* @param excludes classes and resources to exclude
*/
public Relocation(String pattern, String relocatedPattern, Collection<String> includes, Collection<String> excludes) {
this.pattern = requireNonNull(pattern, "pattern").replace("{}", ".");
this.relocatedPattern = requireNonNull(relocatedPattern, "relocatedPattern").replace("{}", ".");
this.includes = includes != null ? Collections.unmodifiableList(new LinkedList<>(includes)) : Collections.emptyList();
this.excludes = excludes != null ? Collections.unmodifiableList(new LinkedList<>(excludes)) : Collections.emptyList();
}
/**
* Creates a new relocation with empty includes and excludes.
*
* @param pattern search pattern
* @param relocatedPattern replacement pattern
*/
public Relocation(String pattern, String relocatedPattern) {
this(pattern, relocatedPattern, null, null);
}
/**
* Gets the search pattern.
*
* @return pattern to search
*/
public String getPattern() {
return pattern;
}
/**
* Gets the replacement pattern.
*
* @return pattern to replace with
*/
public String getRelocatedPattern() {
return relocatedPattern;
}
/**
* Gets included classes and resources.
*
* @return classes and resources to include
*/
public Collection<String> getIncludes() {
return includes;
}
/**
* Gets excluded classes and resources.
*
* @return classes and resources to exclude
*/
public Collection<String> getExcludes() {
return excludes;
}
/**
* Creates a new relocation builder.
*
* @return new relocation builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Provides an alternative method of creating a {@link Relocation}. This
* builder may be more intuitive for configuring relocations that also have
* any includes or excludes.
*/
public static class Builder {
/**
* Search pattern
*/
private String pattern;
/**
* Replacement pattern
*/
private String relocatedPattern;
/**
* Classes and resources to include
*/
private final Collection<String> includes = new LinkedList<>();
/**
* Classes and resources to exclude
*/
private final Collection<String> excludes = new LinkedList<>();
/**
* Sets the search pattern.
*
* @param pattern pattern to search
* @return this builder
*/
public Builder pattern(String pattern) {
this.pattern = requireNonNull(pattern, "pattern");
return this;
}
/**
* Sets the replacement pattern.
*
* @param relocatedPattern pattern to replace with
* @return this builder
*/
public Builder relocatedPattern(String relocatedPattern) {
this.relocatedPattern = requireNonNull(relocatedPattern, "relocatedPattern");
return this;
}
/**
* Adds a class or resource to be included.
*
* @param include class or resource to include
* @return this builder
*/
public Builder include(String include) {
includes.add(requireNonNull(include, "include"));
return this;
}
/**
* Adds a class or resource to be excluded.
*
* @param exclude class or resource to exclude
* @return this builder
*/
public Builder exclude(String exclude) {
excludes.add(requireNonNull(exclude, "exclude"));
return this;
}
/**
* Creates a new relocation using this builder's configuration.
*
* @return new relocation
*/
public Relocation build() {
return new Relocation(pattern, relocatedPattern, includes, excludes);
}
}
}

View File

@ -0,0 +1,130 @@
package net.byteflux.libby.relocation;
import net.byteflux.libby.Library;
import net.byteflux.libby.LibraryManager;
import net.byteflux.libby.Repositories;
import net.byteflux.libby.classloader.IsolatedClassLoader;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import static java.util.Objects.requireNonNull;
/**
* A reflection-based helper for relocating library jars. It automatically
* downloads and invokes Luck's Jar Relocator to perform jar relocations.
*
* @see <a href="https://github.com/lucko/jar-relocator">Luck's Jar Relocator</a>
*/
public class RelocationHelper {
/**
* Reflected constructor for creating new jar relocator instances
*/
private final Constructor<?> jarRelocatorConstructor;
/**
* Reflected method for running a jar relocator
*/
private final Method jarRelocatorRunMethod;
/**
* Reflected constructor for creating relocation instances
*/
private final Constructor<?> relocationConstructor;
/**
* Creates a new relocation helper using the provided library manager to
* download the dependencies required for runtime relocation.
*
* @param libraryManager the library manager used to download dependencies
*/
public RelocationHelper(LibraryManager libraryManager) {
requireNonNull(libraryManager, "libraryManager");
IsolatedClassLoader classLoader = new IsolatedClassLoader();
// ObjectWeb ASM Commons
classLoader.addPath(libraryManager.downloadLibrary(
Library.builder()
.groupId("org.ow2.asm")
.artifactId("asm-commons")
.version("9.7")
.checksum("OJvCR5WOBJ/JoECNOYySxtNwwYA1EgOV1Muh2dkwS3o=")
.repository(Repositories.MAVEN_CENTRAL)
.build()
));
// ObjectWeb ASM
classLoader.addPath(libraryManager.downloadLibrary(
Library.builder()
.groupId("org.ow2.asm")
.artifactId("asm")
.version("9.7")
.checksum("rfRtXjSUC98Ujs3Sap7o7qlElqcgNP9xQQZrPupcTp0=")
.repository(Repositories.MAVEN_CENTRAL)
.build()
));
// Luck's Jar Relocator
classLoader.addPath(libraryManager.downloadLibrary(
Library.builder()
.groupId("me.lucko")
.artifactId("jar-relocator")
.version("1.7")
.checksum("b30RhOF6kHiHl+O5suNLh/+eAr1iOFEFLXhwkHHDu4I=")
.repository(Repositories.MAVEN_CENTRAL)
.build()
));
try {
Class<?> jarRelocatorClass = classLoader.loadClass("me.lucko.jarrelocator.JarRelocator");
Class<?> relocationClass = classLoader.loadClass("me.lucko.jarrelocator.Relocation");
// me.lucko.jarrelocator.JarRelocator(File, File, Collection)
jarRelocatorConstructor = jarRelocatorClass.getConstructor(File.class, File.class, Collection.class);
// me.lucko.jarrelocator.JarRelocator#run()
jarRelocatorRunMethod = jarRelocatorClass.getMethod("run");
// me.lucko.jarrelocator.Relocation(String, String, Collection, Collection)
relocationConstructor = relocationClass.getConstructor(String.class, String.class, Collection.class, Collection.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
/**
* Invokes the jar relocator to process the input jar and generate an
* output jar with the provided relocation rules applied.
*
* @param in input jar
* @param out output jar
* @param relocations relocations to apply
*/
public void relocate(Path in, Path out, Collection<Relocation> relocations) {
requireNonNull(in, "in");
requireNonNull(out, "out");
requireNonNull(relocations, "relocations");
try {
List<Object> rules = new LinkedList<>();
for (Relocation relocation : relocations) {
rules.add(relocationConstructor.newInstance(
relocation.getPattern(),
relocation.getRelocatedPattern(),
relocation.getIncludes(),
relocation.getExcludes()
));
}
jarRelocatorRunMethod.invoke(jarRelocatorConstructor.newInstance(in.toFile(), out.toFile(), rules));
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}