diff --git a/paper-api/pom.xml b/paper-api/pom.xml index 34223ab2c5..32174818dd 100644 --- a/paper-api/pom.xml +++ b/paper-api/pom.xml @@ -57,6 +57,25 @@ 1.27 compile + + + org.apache.maven + maven-resolver-provider + 3.8.1 + provided + + + org.apache.maven.resolver + maven-resolver-connector-basic + 1.6.2 + provided + + + org.apache.maven.resolver + maven-resolver-transport-http + 1.6.2 + provided + 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. + *

+ *

+ * 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 serializable = clazz.asSubclass(ConfigurationSerializable.class); - ConfigurationSerialization.registerClass(serializable); - } + if (ConfigurationSerializable.class.isAssignableFrom(clazz)) { + Class 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 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 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) {