new-1.0
This commit is contained in:
parent
dde365448e
commit
706de6f042
5
pom.xml
5
pom.xml
|
@ -113,28 +113,33 @@
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-compress</artifactId>
|
<artifactId>commons-compress</artifactId>
|
||||||
<version>1.26.2</version>
|
<version>1.26.2</version>
|
||||||
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.luben</groupId>
|
<groupId>com.github.luben</groupId>
|
||||||
<artifactId>zstd-jni</artifactId>
|
<artifactId>zstd-jni</artifactId>
|
||||||
<version>1.5.6-3</version>
|
<version>1.5.6-3</version>
|
||||||
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.zaxxer</groupId>
|
<groupId>com.zaxxer</groupId>
|
||||||
<artifactId>HikariCP</artifactId>
|
<artifactId>HikariCP</artifactId>
|
||||||
<version>4.0.3</version>
|
<version>4.0.3</version>
|
||||||
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
|
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xerial</groupId>
|
<groupId>org.xerial</groupId>
|
||||||
<artifactId>sqlite-jdbc</artifactId>
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
<version>3.46.0.0</version>
|
<version>3.46.0.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
|
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>5.1.3</version>
|
<version>5.1.3</version>
|
||||||
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -3,21 +3,47 @@ package com.io.yutian.elementoriginlib;
|
||||||
import com.io.yutian.elementoriginlib.lang.Lang;
|
import com.io.yutian.elementoriginlib.lang.Lang;
|
||||||
import com.io.yutian.elementoriginlib.listener.GuiHandlerListener;
|
import com.io.yutian.elementoriginlib.listener.GuiHandlerListener;
|
||||||
import com.io.yutian.elementoriginlib.listener.PlayerChatInputListener;
|
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.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
|
||||||
public final class ElementOriginLib extends JavaPlugin {
|
public final class ElementOriginLib extends JavaPlugin {
|
||||||
|
|
||||||
|
private static final org.slf4j.Logger log = LoggerFactory.getLogger(ElementOriginLib.class);
|
||||||
private static ElementOriginLib instance;
|
private static ElementOriginLib instance;
|
||||||
|
|
||||||
|
private static Logger logger = Logger.getLogger(ElementOriginLib.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
instance = this;
|
instance = this;
|
||||||
|
|
||||||
|
loadLibraries();
|
||||||
|
|
||||||
Lang.registerLangFile(this);
|
Lang.registerLangFile(this);
|
||||||
Lang.reload();
|
Lang.reload();
|
||||||
|
|
||||||
new GuiHandlerListener(this);
|
new GuiHandlerListener(this);
|
||||||
new PlayerChatInputListener(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
|
@Override
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package com.io.yutian.elementoriginlib.nbt;
|
package com.io.yutian.elementoriginlib.nbt;
|
||||||
|
|
||||||
import net.minecraft.nbt.NBTTagCompound;
|
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.bukkit.inventory.ItemStack;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
|
@ -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.ItemStackSerializer;
|
||||||
import com.io.yutian.elementoriginlib.serialize.serializers.UUIDSerializer;
|
import com.io.yutian.elementoriginlib.serialize.serializers.UUIDSerializer;
|
||||||
import com.io.yutian.elementoriginlib.util.ReflectionUtil;
|
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.bukkit.inventory.ItemStack;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
|
@ -3,7 +3,7 @@ package com.io.yutian.elementoriginlib.serialize.serializers;
|
||||||
import com.io.yutian.elementoriginlib.serialize.Serializer;
|
import com.io.yutian.elementoriginlib.serialize.Serializer;
|
||||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
import net.minecraft.nbt.*;
|
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.bukkit.inventory.ItemStack;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
@ -52,7 +52,7 @@ public class ItemStackSerializer implements Serializer<ItemStack> {
|
||||||
return tagByteArray.e();
|
return tagByteArray.e();
|
||||||
case 8:
|
case 8:
|
||||||
NBTTagString nbtTagString = (NBTTagString) nbtBase;
|
NBTTagString nbtTagString = (NBTTagString) nbtBase;
|
||||||
return nbtTagString.m_();
|
return nbtTagString.t_();
|
||||||
case 9:
|
case 9:
|
||||||
NBTTagList nbtTagList = (NBTTagList) nbtBase;
|
NBTTagList nbtTagList = (NBTTagList) nbtBase;
|
||||||
JSONArray jsonArray = new JSONArray();
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
|
50
src/main/java/net/byteflux/libby/BukkitLibraryManager.java
Normal file
50
src/main/java/net/byteflux/libby/BukkitLibraryManager.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
16
src/main/java/net/byteflux/libby/LibbyProperties.java
Normal file
16
src/main/java/net/byteflux/libby/LibbyProperties.java
Normal 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;
|
||||||
|
}
|
533
src/main/java/net/byteflux/libby/Library.java
Normal file
533
src/main/java/net/byteflux/libby/Library.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
596
src/main/java/net/byteflux/libby/LibraryManager.java
Normal file
596
src/main/java/net/byteflux/libby/LibraryManager.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
src/main/java/net/byteflux/libby/PaperLibraryManager.java
Normal file
85
src/main/java/net/byteflux/libby/PaperLibraryManager.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
31
src/main/java/net/byteflux/libby/Repositories.java
Normal file
31
src/main/java/net/byteflux/libby/Repositories.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/main/java/net/byteflux/libby/logging/LogLevel.java
Normal file
26
src/main/java/net/byteflux/libby/logging/LogLevel.java
Normal 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
|
||||||
|
}
|
195
src/main/java/net/byteflux/libby/logging/Logger.java
Normal file
195
src/main/java/net/byteflux/libby/logging/Logger.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
184
src/main/java/net/byteflux/libby/relocation/Relocation.java
Normal file
184
src/main/java/net/byteflux/libby/relocation/Relocation.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user