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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,596 @@
package net.byteflux.libby;
import net.byteflux.libby.classloader.IsolatedClassLoader;
import net.byteflux.libby.logging.LogLevel;
import net.byteflux.libby.logging.Logger;
import net.byteflux.libby.logging.adapters.LogAdapter;
import net.byteflux.libby.relocation.Relocation;
import net.byteflux.libby.relocation.RelocationHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Objects.requireNonNull;
/**
* A runtime dependency manager for plugins.
* <p>
* The library manager can resolve a dependency jar through the configured
* Maven repositories, download it into a local cache, relocate it and then
* load it into the plugin's classpath.
* <p>
* Transitive dependencies for a library aren't downloaded automatically and
* must be explicitly loaded like every other library.
* <p>
* It's recommended that libraries are relocated to prevent any namespace
* conflicts with different versions of the same library bundled with other
* plugins or maybe even bundled with the server itself.
*
* @see Library
*/
public abstract class LibraryManager {
/**
* Wrapped plugin logger
*/
protected final Logger logger;
/**
* Directory where downloaded library jars are saved to
*/
protected final Path saveDirectory;
/**
* Maven repositories used to resolve artifacts
*/
private final Set<String> repositories = new LinkedHashSet<>();
/**
* Lazily-initialized relocation helper that uses reflection to call into
* Luck's Jar Relocator
*/
private RelocationHelper relocator;
/**
* Map of isolated class loaders and theirs id
*/
private final Map<String, IsolatedClassLoader> isolatedLibraries = new HashMap<>();
/**
* Creates a new library manager.
*
* @param logAdapter plugin logging adapter
* @param dataDirectory plugin's data directory
*
* @deprecated Use {@link LibraryManager#LibraryManager(LogAdapter, Path, String)}
*/
@Deprecated
protected LibraryManager(LogAdapter logAdapter, Path dataDirectory) {
logger = new Logger(requireNonNull(logAdapter, "logAdapter"));
saveDirectory = requireNonNull(dataDirectory, "dataDirectory").toAbsolutePath().resolve("lib");
}
/**
* Creates a new library manager.
*
* @param logAdapter plugin logging adapter
* @param dataDirectory plugin's data directory
* @param directoryName download directory name
*/
protected LibraryManager(LogAdapter logAdapter, Path dataDirectory, String directoryName) {
logger = new Logger(requireNonNull(logAdapter, "logAdapter"));
saveDirectory = requireNonNull(dataDirectory, "dataDirectory").toAbsolutePath().resolve(requireNonNull(directoryName, "directoryName"));
}
/**
* Adds a file to the plugin's classpath.
*
* @param file the file to add
*/
protected abstract void addToClasspath(Path file);
/**
* Adds a file to the isolated class loader
*
* @param library the library to add
* @param file the file to add
*/
protected void addToIsolatedClasspath(Library library, Path file) {
IsolatedClassLoader classLoader;
String id = library.getId();
if (id != null) {
classLoader = isolatedLibraries.computeIfAbsent(id, s -> new IsolatedClassLoader());
} else {
classLoader = new IsolatedClassLoader();
}
classLoader.addPath(file);
}
/**
* Get the isolated class loader of the library
*
* @param libraryId the id of the library
*/
public IsolatedClassLoader getIsolatedClassLoaderOf(String libraryId) {
return isolatedLibraries.get(libraryId);
}
/**
* Gets the logging level for this library manager.
*
* @return log level
*/
public LogLevel getLogLevel() {
return logger.getLevel();
}
/**
* Sets the logging level for this library manager.
* <p>
* By setting this value, the library manager's logger will not log any
* messages with a level less severe than the configured level. This can be
* useful for silencing the download and relocation logging.
* <p>
* Setting this value to {@link LogLevel#WARN} would silence informational
* logging but still print important things like invalid checksum warnings.
*
* @param level the log level to set
*/
public void setLogLevel(LogLevel level) {
logger.setLevel(level);
}
/**
* Gets the currently added repositories used to resolve artifacts.
* <p>
* For each library this list is traversed to download artifacts after the
* direct download URLs have been attempted.
*
* @return current repositories
*/
public Collection<String> getRepositories() {
List<String> urls;
synchronized (repositories) {
urls = new LinkedList<>(repositories);
}
return Collections.unmodifiableList(urls);
}
/**
* Adds a repository URL to this library manager.
* <p>
* Artifacts will be resolved using this repository when attempts to locate
* the artifact through previously added repositories are all unsuccessful.
*
* @param url repository URL to add
*/
public void addRepository(String url) {
String repo = requireNonNull(url, "url").endsWith("/") ? url : url + '/';
synchronized (repositories) {
repositories.add(repo);
}
}
/**
* Adds the current user's local Maven repository.
*/
public void addMavenLocal() {
addRepository(Paths.get(System.getProperty("user.home")).resolve(".m2/repository").toUri().toString());
}
/**
* Adds the Maven Central repository.
*/
public void addMavenCentral() {
addRepository(Repositories.MAVEN_CENTRAL);
}
/**
* Adds the Sonatype OSS repository.
*/
public void addSonatype() {
addRepository(Repositories.SONATYPE);
}
/**
* Adds the Bintray JCenter repository.
*/
public void addJCenter() {
addRepository(Repositories.JCENTER);
}
/**
* Adds the JitPack repository.
*/
public void addJitPack() {
addRepository(Repositories.JITPACK);
}
/**
* Gets all of the possible download URLs for this library. Entries are
* ordered by direct download URLs first and then repository download URLs.
* <br>This method also resolves SNAPSHOT artifacts URLs.
*
* @param library the library to resolve
* @return download URLs
*/
public Collection<String> resolveLibrary(Library library) {
Set<String> urls = new LinkedHashSet<>(requireNonNull(library, "library").getUrls());
boolean snapshot = library.isSnapshot();
// Try from library-declared repos first
for (String repository : library.getRepositories()) {
if (snapshot) {
String url = resolveSnapshot(repository, library);
if (url != null)
urls.add(repository + url);
} else {
urls.add(repository + library.getPath());
}
}
for (String repository : getRepositories()) {
if (snapshot) {
String url = resolveSnapshot(repository, library);
if (url != null)
urls.add(repository + url);
} else {
urls.add(repository + library.getPath());
}
}
return Collections.unmodifiableSet(urls);
}
/**
* Resolves the URL of the artifact of a snapshot library.
*
* @param repository The repository to query for snapshot information
* @param library The library
* @return The URl of the artifact of a snapshot library or null if no information could be gathered from the
* provided repository
*/
private String resolveSnapshot(String repository, Library library) {
String url = requireNonNull(repository, "repository") + requireNonNull(library, "library").getPartialPath() + "maven-metadata.xml";
try {
URLConnection connection = new URL(requireNonNull(url, "url")).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", LibbyProperties.HTTP_USER_AGENT);
try (InputStream in = connection.getInputStream()) {
return getURLFromMetadata(in, library);
}
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
if (e instanceof FileNotFoundException) {
logger.debug("File not found: " + url);
} else if (e instanceof SocketTimeoutException) {
logger.debug("Connect timed out: " + url);
} else if (e instanceof UnknownHostException) {
logger.debug("Unknown host: " + url);
} else {
logger.debug("Unexpected IOException", e);
}
return null;
}
}
/**
* Gets the URL of the artifact of a snapshot library from the provided InputStream, which should be opened to the
* library's maven-metadata.xml.
*
* @param inputStream The InputStream opened to the library's maven-metadata.xml
* @param library The library
* @return The URl of the artifact of a snapshot library or null if no information could be gathered from the
* provided inputStream
* @throws IOException If any IO errors occur
*/
private String getURLFromMetadata(InputStream inputStream, Library library) throws IOException {
requireNonNull(inputStream, "inputStream");
requireNonNull(library, "library");
String timestamp, buildNumber;
try {
// This reads the maven-metadata.xml file and gets the snapshot info from the <snapshot> tag.
// Example tag:
// <snapshot>
// <timestamp>20220617.013635</timestamp>
// <buildNumber>12</buildNumber>
// </snapshot>
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(inputStream);
doc.getDocumentElement().normalize();
NodeList nodes = doc.getElementsByTagName("snapshot");
if (nodes.getLength() == 0) {
return null;
}
Node snapshot = nodes.item(0);
if (snapshot.getNodeType() != Node.ELEMENT_NODE) {
return null;
}
Node timestampNode = ((Element) snapshot).getElementsByTagName("timestamp").item(0);
if (timestampNode == null || timestampNode.getNodeType() != Node.ELEMENT_NODE) {
return null;
}
Node buildNumberNode = ((Element) snapshot).getElementsByTagName("buildNumber").item(0);
if (buildNumberNode == null || buildNumberNode.getNodeType() != Node.ELEMENT_NODE) {
return null;
}
Node timestampChild = timestampNode.getFirstChild();
if (timestampChild == null || timestampChild.getNodeType() != Node.TEXT_NODE) {
return null;
}
Node buildNumberChild = buildNumberNode.getFirstChild();
if (buildNumberChild == null || buildNumberChild.getNodeType() != Node.TEXT_NODE) {
return null;
}
timestamp = timestampChild.getNodeValue();
buildNumber = buildNumberChild.getNodeValue();
} catch (ParserConfigurationException | SAXException e) {
logger.debug("Invalid maven-metadata.xml", e);
return null;
}
String version = library.getVersion();
// Call .substring(...) only on versions ending in "-SNAPSHOT".
// It should never happen that a snapshot version doesn't end in "-SNAPSHOT", but better be sure
if (version.endsWith("-SNAPSHOT")) {
version = version.substring(0, version.length() - "-SNAPSHOT".length());
}
String url = library.getPartialPath() + library.getArtifactId() + '-' + version + '-' + timestamp + '-' + buildNumber;
if (library.hasClassifier()) {
url += '-' + library.getClassifier();
}
return url + ".jar";
}
/**
* Downloads a library jar and returns the contents as a byte array.
*
* @param url the URL to the library jar
* @return downloaded jar as byte array or null if nothing was downloaded
*/
private byte[] downloadLibrary(String url) {
try {
URLConnection connection = new URL(requireNonNull(url, "url")).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", LibbyProperties.HTTP_USER_AGENT);
try (InputStream in = connection.getInputStream()) {
int len;
byte[] buf = new byte[8192];
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
} catch (SocketTimeoutException e) {
logger.warn("Download timed out: " + connection.getURL());
return null;
}
logger.info("Downloaded library " + connection.getURL());
return out.toByteArray();
}
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
if (e instanceof FileNotFoundException) {
logger.debug("File not found: " + url);
} else if (e instanceof SocketTimeoutException) {
logger.debug("Connect timed out: " + url);
} else if (e instanceof UnknownHostException) {
logger.debug("Unknown host: " + url);
} else {
logger.debug("Unexpected IOException", e);
}
return null;
}
}
/**
* Downloads a library jar to the save directory if it doesn't already
* exist (snapshot libraries are always re-downloaded) and returns
* the local file path.
* <p>
* If the library has a checksum, it will be compared against the
* downloaded jar's checksum to verify the integrity of the download. If
* the checksums don't match, a warning is generated and the next download
* URL is attempted.
* <p>
* Checksum comparison is ignored if the library doesn't have a checksum
* or if the library jar already exists in the save directory.
* <p>
* Most of the time it is advised to use {@link #loadLibrary(Library)}
* instead of this method because this one is only concerned with
* downloading the jar and returning the local path. It's usually more
* desirable to download the jar and add it to the plugin's classpath in
* one operation.
*
* @param library the library to download
* @return local file path to library
* @see #loadLibrary(Library)
*/
public Path downloadLibrary(Library library) {
Path file = saveDirectory.resolve(requireNonNull(library, "library").getPath());
if (Files.exists(file)) {
// Early return only if library isn't a snapshot, since snapshot libraries are always re-downloaded
if (!library.isSnapshot()) {
return file;
}
// Delete the file since the Files.move call down below will fail if it exists
try {
Files.delete(file);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
Collection<String> urls = resolveLibrary(library);
if (urls.isEmpty()) {
throw new RuntimeException("Library '" + library + "' couldn't be resolved, add a repository");
}
MessageDigest md = null;
if (library.hasChecksum()) {
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
Path out = file.resolveSibling(file.getFileName() + ".tmp");
out.toFile().deleteOnExit();
try {
Files.createDirectories(file.getParent());
for (String url : urls) {
byte[] bytes = downloadLibrary(url);
if (bytes == null) {
continue;
}
if (md != null) {
byte[] checksum = md.digest(bytes);
if (!Arrays.equals(checksum, library.getChecksum())) {
logger.warn("*** INVALID CHECKSUM ***");
logger.warn(" Library : " + library);
logger.warn(" URL : " + url);
logger.warn(" Expected : " + Base64.getEncoder().encodeToString(library.getChecksum()));
logger.warn(" Actual : " + Base64.getEncoder().encodeToString(checksum));
continue;
}
}
Files.write(out, bytes);
Files.move(out, file);
return file;
}
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
try {
Files.deleteIfExists(out);
} catch (IOException ignored) {
}
}
throw new RuntimeException("Failed to download library '" + library + "'");
}
/**
* Processes the input jar and generates an output jar with the provided
* relocation rules applied, then returns the path to the relocated jar.
*