org.jetbrains
diff --git a/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java b/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
index f78a7df6e2..801a323d9c 100644
--- a/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
+++ b/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
@@ -133,6 +133,10 @@ import org.yaml.snakeyaml.nodes.Tag;
* api-version |
* {@link #getAPIVersion()} |
* The API version which this plugin was programmed against |
+ *
+ * libraries |
+ * {@link #getLibraries() ()} |
+ * The libraries to be linked with this plugin |
*
*
*
@@ -152,6 +156,8 @@ import org.yaml.snakeyaml.nodes.Tag;
*main: com.captaininflamo.bukkit.inferno.Inferno
*depend: [NewFire, FlameWire]
*api-version: 1.13
+ *libraries:
+ - com.squareup.okhttp3:okhttp:4.9.0
*
*commands:
* flagrate:
@@ -247,6 +253,7 @@ public final class PluginDescriptionFile {
private PermissionDefault defaultPerm = PermissionDefault.OP;
private Set awareness = ImmutableSet.of();
private String apiVersion = null;
+ private List libraries = ImmutableList.of();
public PluginDescriptionFile(@NotNull final InputStream stream) throws InvalidDescriptionException {
loadMap(asMap(YAML.get().load(stream)));
@@ -957,6 +964,22 @@ public final class PluginDescriptionFile {
return apiVersion;
}
+ /**
+ * Gets the libraries this plugin requires. This is a preview feature.
+ *
+ * - Libraries must be GAV specifiers and are loaded from Maven Central.
+ *
+ *
+ * Example:
libraries:
+ * - com.squareup.okhttp3:okhttp:4.9.0
+ *
+ * @return required libraries
+ */
+ @NotNull
+ public List getLibraries() {
+ return libraries;
+ }
+
/**
* @return unused
* @deprecated unused
@@ -1127,6 +1150,20 @@ public final class PluginDescriptionFile {
apiVersion = map.get("api-version").toString();
}
+ if (map.get("libraries") != null) {
+ ImmutableList.Builder contributorsBuilder = ImmutableList.builder();
+ try {
+ for (Object o : (Iterable>) map.get("libraries")) {
+ contributorsBuilder.add(o.toString());
+ }
+ } catch (ClassCastException ex) {
+ throw new InvalidDescriptionException(ex, "libraries are of wrong type");
+ }
+ libraries = contributorsBuilder.build();
+ } else {
+ libraries = ImmutableList.of();
+ }
+
try {
lazyPermissions = (Map, ?>) map.get("permissions");
} catch (ClassCastException ex) {
@@ -1201,6 +1238,10 @@ public final class PluginDescriptionFile {
map.put("api-version", apiVersion);
}
+ if (libraries != null) {
+ map.put("libraries", libraries);
+ }
+
if (classLoaderOf != null) {
map.put("class-loader-of", classLoaderOf);
}
diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
index 765e880596..390abd5115 100644
--- a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+++ b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
@@ -7,12 +7,12 @@ import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@@ -38,6 +38,7 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoader;
import org.bukkit.plugin.RegisteredListener;
+import org.bukkit.plugin.SimplePluginManager;
import org.bukkit.plugin.TimedRegisteredListener;
import org.bukkit.plugin.UnknownDependencyException;
import org.jetbrains.annotations.NotNull;
@@ -50,8 +51,8 @@ import org.yaml.snakeyaml.error.YAMLException;
public final class JavaPluginLoader implements PluginLoader {
final Server server;
private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")};
- private final Map> classes = new ConcurrentHashMap>();
private final List loaders = new CopyOnWriteArrayList();
+ private final LibraryLoader libraryLoader;
/**
* This class was not meant to be constructed explicitly
@@ -62,6 +63,15 @@ public final class JavaPluginLoader implements PluginLoader {
public JavaPluginLoader(@NotNull Server instance) {
Validate.notNull(instance, "Server cannot be null");
server = instance;
+
+ LibraryLoader libraryLoader = null;
+ try {
+ libraryLoader = new LibraryLoader(server.getLogger());
+ } catch (NoClassDefFoundError ex) {
+ // Provided depends were not added back
+ server.getLogger().warning("Could not initialize LibraryLoader (missing dependencies?)");
+ }
+ this.libraryLoader = libraryLoader;
}
@Override
@@ -130,7 +140,7 @@ public final class JavaPluginLoader implements PluginLoader {
final PluginClassLoader loader;
try {
- loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file);
+ loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file, (libraryLoader != null) ? libraryLoader.createLoader(description) : null);
} catch (InvalidPluginException ex) {
throw ex;
} catch (Throwable ex) {
@@ -189,46 +199,27 @@ public final class JavaPluginLoader implements PluginLoader {
}
@Nullable
- Class> getClassByName(final String name) {
- Class> cachedClass = classes.get(name);
-
- if (cachedClass != null) {
- return cachedClass;
- } else {
- for (PluginClassLoader loader : loaders) {
- try {
- cachedClass = loader.findClass(name, false);
- } catch (ClassNotFoundException cnfe) {}
- if (cachedClass != null) {
- return cachedClass;
- }
+ Class> getClassByName(final String name, boolean resolve, PluginDescriptionFile description) {
+ for (PluginClassLoader loader : loaders) {
+ try {
+ return loader.loadClass0(name, resolve, false, ((SimplePluginManager) server.getPluginManager()).isTransitiveDepend(description, loader.plugin.getDescription()));
+ } catch (ClassNotFoundException cnfe) {
}
}
return null;
}
void setClass(@NotNull final String name, @NotNull final Class> clazz) {
- if (!classes.containsKey(name)) {
- classes.put(name, clazz);
-
- if (ConfigurationSerializable.class.isAssignableFrom(clazz)) {
- Class extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class);
- ConfigurationSerialization.registerClass(serializable);
- }
+ if (ConfigurationSerializable.class.isAssignableFrom(clazz)) {
+ Class extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class);
+ ConfigurationSerialization.registerClass(serializable);
}
}
- private void removeClass(@NotNull String name) {
- Class> clazz = classes.remove(name);
-
- try {
- if ((clazz != null) && (ConfigurationSerializable.class.isAssignableFrom(clazz))) {
- Class extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class);
- ConfigurationSerialization.unregisterClass(serializable);
- }
- } catch (NullPointerException ex) {
- // Boggle!
- // (Native methods throwing NPEs is not fun when you can't stop it before-hand)
+ private void removeClass(@NotNull Class> clazz) {
+ if (ConfigurationSerializable.class.isAssignableFrom(clazz)) {
+ Class extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class);
+ ConfigurationSerialization.unregisterClass(serializable);
}
}
@@ -374,10 +365,16 @@ public final class JavaPluginLoader implements PluginLoader {
PluginClassLoader loader = (PluginClassLoader) cloader;
loaders.remove(loader);
- Set names = loader.getClasses();
+ Collection> classes = loader.getClasses();
- for (String name : names) {
- removeClass(name);
+ for (Class> clazz : classes) {
+ removeClass(clazz);
+ }
+
+ try {
+ loader.close();
+ } catch (IOException ex) {
+ //
}
}
}
diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/paper-api/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
new file mode 100644
index 0000000000..939afec1f2
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
@@ -0,0 +1,128 @@
+// CHECKSTYLE:OFF
+package org.bukkit.plugin.java;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.impl.DefaultServiceLocator;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResolutionException;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transport.http.HttpTransporterFactory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+class LibraryLoader
+{
+
+ private final Logger logger;
+ private final RepositorySystem repository;
+ private final DefaultRepositorySystemSession session;
+ private final List repositories;
+
+ public LibraryLoader(@NotNull Logger logger)
+ {
+ this.logger = logger;
+
+ DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
+ locator.addService( RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class );
+ locator.addService( TransporterFactory.class, HttpTransporterFactory.class );
+
+ this.repository = locator.getService( RepositorySystem.class );
+ this.session = MavenRepositorySystemUtils.newSession();
+
+ session.setChecksumPolicy( RepositoryPolicy.CHECKSUM_POLICY_FAIL );
+ session.setLocalRepositoryManager( repository.newLocalRepositoryManager( session, new LocalRepository( "libraries" ) ) );
+ session.setTransferListener( new AbstractTransferListener()
+ {
+ @Override
+ public void transferStarted(@NotNull TransferEvent event) throws TransferCancelledException
+ {
+ logger.log( Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName() );
+ }
+ } );
+ session.setReadOnly();
+
+ this.repositories = repository.newResolutionRepositories( session, Arrays.asList( new RemoteRepository.Builder( "central", "default", "https://repo.maven.apache.org/maven2" ).build() ) );
+ }
+
+ @Nullable
+ public ClassLoader createLoader(@NotNull PluginDescriptionFile desc)
+ {
+ if ( desc.getLibraries().isEmpty() )
+ {
+ return null;
+ }
+ logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[]
+ {
+ desc.getName(), desc.getLibraries().size()
+ } );
+
+ List dependencies = new ArrayList<>();
+ for ( String library : desc.getLibraries() )
+ {
+ Artifact artifact = new DefaultArtifact( library );
+ Dependency dependency = new Dependency( artifact, null );
+
+ dependencies.add( dependency );
+ }
+
+ DependencyResult result;
+ try
+ {
+ result = repository.resolveDependencies( session, new DependencyRequest( new CollectRequest( (Dependency) null, dependencies, repositories ), null ) );
+ } catch ( DependencyResolutionException ex )
+ {
+ throw new RuntimeException( "Error resolving libraries", ex );
+ }
+
+ List jarFiles = new ArrayList<>();
+ for ( ArtifactResult artifact : result.getArtifactResults() )
+ {
+ File file = artifact.getArtifact().getFile();
+
+ URL url;
+ try
+ {
+ url = file.toURI().toURL();
+ } catch ( MalformedURLException ex )
+ {
+ throw new AssertionError( ex );
+ }
+
+ jarFiles.add( url );
+ logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[]
+ {
+ desc.getName(), file
+ } );
+ }
+
+ URLClassLoader loader = new URLClassLoader( jarFiles.toArray( new URL[ jarFiles.size() ] ) );
+
+ return loader;
+ }
+}
diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
index 5830e8b9b7..6843e32438 100644
--- a/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
+++ b/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
@@ -9,6 +9,7 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSigner;
import java.security.CodeSource;
+import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
@@ -37,6 +38,7 @@ final class PluginClassLoader extends URLClassLoader {
private final JarFile jar;
private final Manifest manifest;
private final URL url;
+ private final ClassLoader libraryLoader;
final JavaPlugin plugin;
private JavaPlugin pluginInit;
private IllegalStateException pluginState;
@@ -46,7 +48,7 @@ final class PluginClassLoader extends URLClassLoader {
ClassLoader.registerAsParallelCapable();
}
- PluginClassLoader(@NotNull final JavaPluginLoader loader, @Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file) throws IOException, InvalidPluginException, MalformedURLException {
+ PluginClassLoader(@NotNull final JavaPluginLoader loader, @Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file, @Nullable ClassLoader libraryLoader) throws IOException, InvalidPluginException, MalformedURLException {
super(new URL[] {file.toURI().toURL()}, parent);
Validate.notNull(loader, "Loader cannot be null");
@@ -57,6 +59,7 @@ final class PluginClassLoader extends URLClassLoader {
this.jar = new JarFile(file);
this.manifest = jar.getManifest();
this.url = file.toURI().toURL();
+ this.libraryLoader = libraryLoader;
try {
Class> jarClass;
@@ -92,87 +95,101 @@ final class PluginClassLoader extends URLClassLoader {
}
@Override
- protected Class> findClass(String name) throws ClassNotFoundException {
- return findClass(name, true);
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ return loadClass0(name, resolve, true, true);
}
- Class> findClass(@NotNull String name, boolean checkGlobal) throws ClassNotFoundException {
+ Class> loadClass0(@NotNull String name, boolean resolve, boolean checkGlobal, boolean checkLibraries) throws ClassNotFoundException {
+ try {
+ return super.loadClass(name, resolve);
+ } catch (ClassNotFoundException ex) {
+ }
+
+ if (checkLibraries && libraryLoader != null) {
+ try {
+ return libraryLoader.loadClass(name);
+ } catch (ClassNotFoundException ex) {
+ }
+ }
+
+ if (checkGlobal) {
+ Class> result = loader.getClassByName(name, resolve, description);
+
+ if (result != null) {
+ PluginDescriptionFile provider = ((PluginClassLoader) result.getClassLoader()).description;
+
+ if (provider != description
+ && !seenIllegalAccess.contains(provider.getName())
+ && !((SimplePluginManager) loader.server.getPluginManager()).isTransitiveDepend(description, provider)) {
+
+ seenIllegalAccess.add(provider.getName());
+ if (plugin != null) {
+ plugin.getLogger().log(Level.WARNING, "Loaded class {0} from {1} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{name, provider.getFullName()});
+ } else {
+ // In case the bad access occurs on construction
+ loader.server.getLogger().log(Level.WARNING, "[{0}] Loaded class {1} from {2} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{description.getName(), name, provider.getFullName()});
+ }
+ }
+
+ return result;
+ }
+ }
+
+ throw new ClassNotFoundException(name);
+ }
+
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException {
if (name.startsWith("org.bukkit.") || name.startsWith("net.minecraft.")) {
throw new ClassNotFoundException(name);
}
Class> result = classes.get(name);
if (result == null) {
- if (checkGlobal) {
- result = loader.getClassByName(name);
+ String path = name.replace('.', '/').concat(".class");
+ JarEntry entry = jar.getJarEntry(path);
- if (result != null) {
- PluginDescriptionFile provider = ((PluginClassLoader) result.getClassLoader()).description;
+ if (entry != null) {
+ byte[] classBytes;
- if (provider != description
- && !seenIllegalAccess.contains(provider.getName())
- && !((SimplePluginManager) loader.server.getPluginManager()).isTransitiveDepend(description, provider)) {
-
- seenIllegalAccess.add(provider.getName());
- if (plugin != null) {
- plugin.getLogger().log(Level.WARNING, "Loaded class {0} from {1} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{name, provider.getFullName()});
- } else {
- // In case the bad access occurs on construction
- loader.server.getLogger().log(Level.WARNING, "[{0}] Loaded class {1} from {2} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{description.getName(), name, provider.getFullName()});
- }
- }
+ try (InputStream is = jar.getInputStream(entry)) {
+ classBytes = ByteStreams.toByteArray(is);
+ } catch (IOException ex) {
+ throw new ClassNotFoundException(name, ex);
}
- }
- if (result == null) {
- String path = name.replace('.', '/').concat(".class");
- JarEntry entry = jar.getJarEntry(path);
+ classBytes = loader.server.getUnsafe().processClass(description, path, classBytes);
- if (entry != null) {
- byte[] classBytes;
-
- try (InputStream is = jar.getInputStream(entry)) {
- classBytes = ByteStreams.toByteArray(is);
- } catch (IOException ex) {
- throw new ClassNotFoundException(name, ex);
- }
-
- classBytes = loader.server.getUnsafe().processClass(description, path, classBytes);
-
- int dot = name.lastIndexOf('.');
- if (dot != -1) {
- String pkgName = name.substring(0, dot);
- if (getPackage(pkgName) == null) {
- try {
- if (manifest != null) {
- definePackage(pkgName, manifest, url);
- } else {
- definePackage(pkgName, null, null, null, null, null, null, null);
- }
- } catch (IllegalArgumentException ex) {
- if (getPackage(pkgName) == null) {
- throw new IllegalStateException("Cannot find package " + pkgName);
- }
+ int dot = name.lastIndexOf('.');
+ if (dot != -1) {
+ String pkgName = name.substring(0, dot);
+ if (getPackage(pkgName) == null) {
+ try {
+ if (manifest != null) {
+ definePackage(pkgName, manifest, url);
+ } else {
+ definePackage(pkgName, null, null, null, null, null, null, null);
+ }
+ } catch (IllegalArgumentException ex) {
+ if (getPackage(pkgName) == null) {
+ throw new IllegalStateException("Cannot find package " + pkgName);
}
}
}
-
- CodeSigner[] signers = entry.getCodeSigners();
- CodeSource source = new CodeSource(url, signers);
-
- result = defineClass(name, classBytes, 0, classBytes.length, source);
}
- if (result == null) {
- result = super.findClass(name);
- }
+ CodeSigner[] signers = entry.getCodeSigners();
+ CodeSource source = new CodeSource(url, signers);
- if (result != null) {
- loader.setClass(name, result);
- }
-
- classes.put(name, result);
+ result = defineClass(name, classBytes, 0, classBytes.length, source);
}
+
+ if (result == null) {
+ result = super.findClass(name);
+ }
+
+ loader.setClass(name, result);
+ classes.put(name, result);
}
return result;
@@ -188,8 +205,8 @@ final class PluginClassLoader extends URLClassLoader {
}
@NotNull
- Set getClasses() {
- return classes.keySet();
+ Collection> getClasses() {
+ return classes.values();
}
synchronized void initialize(@NotNull JavaPlugin javaPlugin) {