From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> Date: Wed, 6 Jul 2022 23:00:36 -0400 Subject: [PATCH] Paper Plugins diff --git a/build.gradle.kts b/build.gradle.kts index ba6f0a70ba2442dbe60ed6cc92e4fb91a48d9f3b..22b65c3ad4ed6e19b88a51a2e001f0e5846d9ae6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { implementation("org.ow2.asm:asm-commons:9.4") // Paper end - compileOnly("org.apache.maven:maven-resolver-provider:3.8.5") + api("org.apache.maven:maven-resolver-provider:3.8.5") compileOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.3") compileOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.3") compileOnly("com.google.code.findbugs:jsr305:1.3.9") // Paper diff --git a/src/main/java/io/papermc/paper/plugin/PermissionManager.java b/src/main/java/io/papermc/paper/plugin/PermissionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..cdbc93b317b3bab47bf6552c29cfbb2c27846933 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/PermissionManager.java @@ -0,0 +1,171 @@ +package io.papermc.paper.plugin; + +import org.bukkit.permissions.Permissible; +import org.bukkit.permissions.Permission; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +/** + * A permission manager implementation to keep backwards compatibility partially alive with existing plugins that used + * the bukkit one before. + */ +@ApiStatus.Experimental +public interface PermissionManager { + + /** + * Gets a {@link Permission} from its fully qualified name + * + * @param name Name of the permission + * @return Permission, or null if none + */ + @Nullable + Permission getPermission(@NotNull String name); + + /** + * Adds a {@link Permission} to this plugin manager. + *

+ * If a permission is already defined with the given name of the new + * permission, an exception will be thrown. + * + * @param perm Permission to add + * @throws IllegalArgumentException Thrown when a permission with the same + * name already exists + */ + void addPermission(@NotNull Permission perm); + + /** + * Removes a {@link Permission} registration from this plugin manager. + *

+ * If the specified permission does not exist in this plugin manager, + * nothing will happen. + *

+ * Removing a permission registration will not remove the + * permission from any {@link Permissible}s that have it. + * + * @param perm Permission to remove + */ + void removePermission(@NotNull Permission perm); + + /** + * Removes a {@link Permission} registration from this plugin manager. + *

+ * If the specified permission does not exist in this plugin manager, + * nothing will happen. + *

+ * Removing a permission registration will not remove the + * permission from any {@link Permissible}s that have it. + * + * @param name Permission to remove + */ + void removePermission(@NotNull String name); + + /** + * Gets the default permissions for the given op status + * + * @param op Which set of default permissions to get + * @return The default permissions + */ + @NotNull + Set getDefaultPermissions(boolean op); + + /** + * Recalculates the defaults for the given {@link Permission}. + *

+ * This will have no effect if the specified permission is not registered + * here. + * + * @param perm Permission to recalculate + */ + void recalculatePermissionDefaults(@NotNull Permission perm); + + /** + * Subscribes the given Permissible for information about the requested + * Permission, by name. + *

+ * If the specified Permission changes in any form, the Permissible will + * be asked to recalculate. + * + * @param permission Permission to subscribe to + * @param permissible Permissible subscribing + */ + void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible); + + /** + * Unsubscribes the given Permissible for information about the requested + * Permission, by name. + * + * @param permission Permission to unsubscribe from + * @param permissible Permissible subscribing + */ + void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible); + + /** + * Gets a set containing all subscribed {@link Permissible}s to the given + * permission, by name + * + * @param permission Permission to query for + * @return Set containing all subscribed permissions + */ + @NotNull + Set getPermissionSubscriptions(@NotNull String permission); + + /** + * Subscribes to the given Default permissions by operator status + *

+ * If the specified defaults change in any form, the Permissible will be + * asked to recalculate. + * + * @param op Default list to subscribe to + * @param permissible Permissible subscribing + */ + void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible); + + /** + * Unsubscribes from the given Default permissions by operator status + * + * @param op Default list to unsubscribe from + * @param permissible Permissible subscribing + */ + void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible); + + /** + * Gets a set containing all subscribed {@link Permissible}s to the given + * default list, by op status + * + * @param op Default list to query for + * @return Set containing all subscribed permissions + */ + @NotNull + Set getDefaultPermSubscriptions(boolean op); + + /** + * Gets a set of all registered permissions. + *

+ * This set is a copy and will not be modified live. + * + * @return Set containing all current registered permissions + */ + @NotNull + Set getPermissions(); + + /** + * Adds a list of permissions. + *

+ * This is meant as an optimization for adding multiple permissions without recalculating each permission. + * + * @param perm permission + */ + void addPermissions(@NotNull List perm); + + /** + * Clears the current registered permissinos. + *

+ * This is used for reloading. + */ + void clearPermissions(); + +} diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/BootstrapContext.java b/src/main/java/io/papermc/paper/plugin/bootstrap/BootstrapContext.java new file mode 100644 index 0000000000000000000000000000000000000000..08f2050356acaf74e3210416760e3873c2dafd2c --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/bootstrap/BootstrapContext.java @@ -0,0 +1,14 @@ +package io.papermc.paper.plugin.bootstrap; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents the context provided to a {@link PluginBootstrap} during both the bootstrapping and plugin + * instantiation logic. + * A boostrap context may be used to access data or logic usually provided to {@link org.bukkit.plugin.Plugin} instances + * like the plugin's configuration or logger during the plugins bootstrap. + */ +@ApiStatus.Experimental +@ApiStatus.NonExtendable +public interface BootstrapContext extends PluginProviderContext { +} diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrap.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrap.java new file mode 100644 index 0000000000000000000000000000000000000000..288c078da3d3ca78d02caa4e3565ac7cf89f9f9f --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrap.java @@ -0,0 +1,41 @@ +package io.papermc.paper.plugin.bootstrap; + +import io.papermc.paper.plugin.provider.util.ProviderUtil; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A plugin boostrap is meant for loading certain parts of the plugin before the server is loaded. + *

+ * Plugin bootstrapping allows values to be initialized in certain parts of the server that might not be allowed + * when the server is running. + *

+ * Your bootstrap class will be on the same classloader as your JavaPlugin. + *

+ * All calls to Bukkit may throw a NullPointerExceptions or return null unexpectedly. You should only call api methods that are explicitly documented to work in the bootstrapper + */ +@ApiStatus.OverrideOnly +@ApiStatus.Experimental +public interface PluginBootstrap { + + /** + * Called by the server, allowing you to bootstrap the plugin with a context that provides things like a logger and your shared plugin configuration file. + * + * @param context the server provided context + */ + void bootstrap(@NotNull BootstrapContext context); + + /** + * Called by the server to instantiate your main class. + * Plugins may override this logic to define custom creation logic for said instance, like passing addition + * constructor arguments. + * + * @param context the server created bootstrap object + * @return the server requested instance of the plugins main class. + */ + @NotNull + default JavaPlugin createPlugin(@NotNull PluginProviderContext context) { + return ProviderUtil.loadClass(context.getConfiguration().getMainClass(), JavaPlugin.class, this.getClass().getClassLoader()); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContext.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContext.java new file mode 100644 index 0000000000000000000000000000000000000000..2c14693155de3654d5ca011c63e13e4a1abf2080 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContext.java @@ -0,0 +1,52 @@ +package io.papermc.paper.plugin.bootstrap; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Represents the context provided to a {@link PluginBootstrap} during both the bootstrapping and plugin + * instantiation logic. + * A boostrap context may be used to access data or logic usually provided to {@link org.bukkit.plugin.Plugin} instances + * like the plugin's configuration or logger during the plugins bootstrap. + */ +@ApiStatus.NonExtendable +@ApiStatus.Experimental +public interface PluginProviderContext { + + /** + * Provides the plugin's configuration. + * + * @return the plugin's configuration + */ + @NotNull + PluginMeta getConfiguration(); + + /** + * Provides the path to the data directory of the plugin. + * + * @return the previously described path + */ + @NotNull + Path getDataDirectory(); + + /** + * Provides the logger used for this plugin. + * + * @return the logger instance + */ + @NotNull + ComponentLogger getLogger(); + + /** + * Provides the path to the originating source of the plugin, such as the plugin's JAR file. + * + * @return the previously described path + */ + @NotNull + Path getPluginSource(); + +} diff --git a/src/main/java/io/papermc/paper/plugin/configuration/PluginMeta.java b/src/main/java/io/papermc/paper/plugin/configuration/PluginMeta.java new file mode 100644 index 0000000000000000000000000000000000000000..ef393f1f93ca48264fc1b6e3a27787f6a9152e1b --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/configuration/PluginMeta.java @@ -0,0 +1,203 @@ +package io.papermc.paper.plugin.configuration; + +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginLoadOrder; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * This class acts as an abstraction for a plugin configuration. + */ +@ApiStatus.NonExtendable +@ApiStatus.Experimental // Subject to change! +public interface PluginMeta { + + /** + * Provides the name of the plugin. This name uniquely identifies the plugin amongst all loaded plugins on the + * server. + *

+ *

+ * In the plugin.yml, this entry is named name. + *

+ * Example:

name: MyPlugin
+ * + * @return the name of the plugin + */ + @NotNull + String getName(); + + /** + * Returns the display name of the plugin, including the version. + * + * @return a descriptive name of the plugin and respective version + */ + @NotNull + default String getDisplayName() { + return this.getName() + " v" + this.getVersion(); + } + + /** + * Provides the fully qualified class name of the main class for the plugin. + * A subtype of {@link JavaPlugin} is expected at this location. + * + * @return the fully qualified class name of the plugin's main class. + */ + @NotNull + String getMainClass(); + + /** + * Returns the phase of the server startup logic that the plugin should be loaded. + * + * @return the plugin load order + * @see PluginLoadOrder for further details regards the available load orders. + */ + @NotNull + PluginLoadOrder getLoadOrder(); + + /** + * Provides the version of this plugin as defined by the plugin. + * There is no inherit format defined/enforced for the version of a plugin, however a common approach + * might be schematic versioning. + * + * @return the string representation of the plugin's version + */ + @NotNull + String getVersion(); + + /** + * Provides the prefix that should be used for the plugin logger. + * The logger prefix allows plugins to overwrite the usual default of the logger prefix, which is the name of the + * plugin. + * + * @return the specific overwrite of the logger prefix as defined by the plugin. If the plugin did not define a + * custom logger prefix, this method will return null + */ + @Nullable + String getLoggerPrefix(); + + /** + * Provides a list of dependencies that are required for this plugin to load. + * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, of the + * dependencies. + *

+ * If any of the dependencies defined by this list are not installed on the server, this plugin will fail to load. + * + * @return an immutable list of required dependency names + */ + @NotNull + List getPluginDependencies(); + + /** + * Provides a list of dependencies that are used but not required by this plugin. + * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, of the soft + * dependencies. + *

+ * If these dependencies are installed on the server, they will be loaded first and supplied as dependencies to this + * plugin, however the plugin will load even if these dependencies are not installed. + * + * @return immutable list of soft dependencies + */ + @NotNull + List getPluginSoftDependencies(); + + /** + * Provides a list of plugins that should be loaded before this plugin is loaded. + * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, of the + * plugins that should be loaded before the plugin described by this plugin meta. + *

+ * The plugins referenced in the list provided by this method are not considered dependencies of this plugin and + * are hence not available to the plugin at runtime. They merely load before this plugin. + * + * @return immutable list of plugins to load before this plugin + */ + @NotNull + List getLoadBeforePlugins(); + + /** + * Returns the list of plugins/dependencies that this plugin provides. + * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, for each plugin + * it provides the expected classes for. + * + * @return immutable list of provided plugins/dependencies + */ + @NotNull + List getProvidedPlugins(); + + /** + * Provides the list of authors that are credited with creating this plugin. + * The author names are in no particular format. + * + * @return an immutable list of the plugin's authors + */ + @NotNull + List getAuthors(); + + /** + * Provides a list of contributors that contributed to the plugin but are not considered authors. + * The names of the contributors are in no particular format. + * + * @return an immutable list of the plugin's contributors + */ + @NotNull + List getContributors(); + + /** + * Gives a human-friendly description of the functionality the plugin + * provides. + * + * @return description or null if the plugin did not define a human readable description. + */ + @Nullable + String getDescription(); + + /** + * Provides the website for the plugin or the plugin's author. + * The defined string value is not guaranteed to be in the form of a url. + * + * @return a string representation of the website that serves as the main hub for this plugin/its author. + */ + @Nullable + String getWebsite(); + + /** + * Provides the list of permissions that are defined via the plugin meta instance. + * + * @return an immutable list of permissions + */ + // TODO: Do we even want this? Why not just use the bootstrapper + @NotNull + List getPermissions(); + + /** + * Provides the default values that apply to the permissions defined in this plugin meta. + * + * @return the bukkit permission default container. + * @see #getPermissions() + */ + // TODO: Do we even want this? Why not just use the bootstrapper + @NotNull + PermissionDefault getPermissionDefault(); + + /** + * Gets the api version that this plugin supports. + * Nullable if this version is not specified, and should be + * considered legacy (spigot plugins only) + * + * @return the version string made up of the major and minor version (e.g. 1.18 or 1.19). Minor versions like 1.18.2 + * are unified to their major release version (in this example 1.18) + */ + @Nullable + String getAPIVersion(); + +} diff --git a/src/main/java/io/papermc/paper/plugin/configuration/package-info.java b/src/main/java/io/papermc/paper/plugin/configuration/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..ddb3076124365d0d1a5caa32d4dcb1f4314dd7ae --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/configuration/package-info.java @@ -0,0 +1,8 @@ +/** + * The paper configuration package contains the new java representation of a plugins configuration file. + * While most values are described in detail on {@link io.papermc.paper.plugin.configuration.PluginMeta}, a full + * entry on the paper contains a full and extensive example of possible configurations of the paper-plugin.yml. + * @see Extensive documentation and examples of the paper-plugin.yml + * + */ +package io.papermc.paper.plugin.configuration; diff --git a/src/main/java/io/papermc/paper/plugin/loader/PluginClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PluginClasspathBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..28cbc09b7c1ded1f4515969cef4a669adac85703 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/PluginClasspathBuilder.java @@ -0,0 +1,38 @@ +package io.papermc.paper.plugin.loader; + +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.loader.library.ClassPathLibrary; +import io.papermc.paper.plugin.loader.library.LibraryStore; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +/** + * A mutable builder that may be used to collect and register all {@link ClassPathLibrary} instances a + * {@link PluginLoader} aims to provide to its plugin at runtime. + */ +@ApiStatus.NonExtendable +@ApiStatus.Experimental +public interface PluginClasspathBuilder { + + /** + * Adds a new classpath library to this classpath builder. + *

+ * As a builder, this method does not invoke {@link ClassPathLibrary#register(LibraryStore)} and + * may hence be run without invoking potential IO performed by a {@link ClassPathLibrary} during resolution. + *

+ * The paper api provides pre implemented {@link ClassPathLibrary} types that allow easy inclusion of existing + * libraries on disk or on remote maven repositories. + * + * @param classPathLibrary the library instance to add to this builder + * @return self + * @see io.papermc.paper.plugin.loader.library.impl.JarLibrary + * @see io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver + */ + @NotNull + @Contract("_ -> this") + PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary); + + @NotNull + PluginProviderContext getContext(); +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/PluginLoader.java b/src/main/java/io/papermc/paper/plugin/loader/PluginLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..c9e31f78ff6ff969436c6d99755845786c4d383f --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/PluginLoader.java @@ -0,0 +1,30 @@ +package io.papermc.paper.plugin.loader; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A plugin loader is responsible for creating certain aspects of a plugin before it is created. + *

+ * The goal of the plugin loader is the creation of an expected/dynamic environment for the plugin to load into. + * This, as of right now, only applies to creating the expected classpath for the plugin, e.g. supplying external + * libraries to the plugin. + *

+ * It should be noted that this class will be called from a different classloader, this will cause any static values + * set in this class/any other classes loaded not to persist when the plugin loads. + */ +@ApiStatus.OverrideOnly +@ApiStatus.Experimental +public interface PluginLoader { + + /** + * Called by the server to allows plugins to configure the runtime classpath that the plugin is run on. + * This allows plugin loaders to configure dependencies for the plugin where jars can be downloaded or + * provided during runtime. + * + * @param classpathBuilder a mutable classpath builder that may be used to register custom runtime dependencies + * for the plugin the loader was registered for. + */ + void classloader(@NotNull PluginClasspathBuilder classpathBuilder); + +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/ClassPathLibrary.java b/src/main/java/io/papermc/paper/plugin/loader/library/ClassPathLibrary.java new file mode 100644 index 0000000000000000000000000000000000000000..1347b535d90c2c281c184d0459e7ac59c0350c9f --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/library/ClassPathLibrary.java @@ -0,0 +1,20 @@ +package io.papermc.paper.plugin.loader.library; + +import org.jetbrains.annotations.NotNull; + +/** + * The classpath library interface represents libraries that are capable of registering themselves via + * {@link #register(LibraryStore)} on any given {@link LibraryStore}. + */ +public interface ClassPathLibrary { + + /** + * Called to register the library this class path library represents into the passed library store. + * This method may either be implemented by the plugins themselves if they need complex logic, or existing + * API exposed implementations of this interface may be used. + * + * @param store the library store instance to register this library into + * @throws LibraryLoadingException if library loading failed for this classpath library + */ + void register(@NotNull LibraryStore store) throws LibraryLoadingException; +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/LibraryLoadingException.java b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryLoadingException.java new file mode 100644 index 0000000000000000000000000000000000000000..79ba423a364b50588f3ee87fdc69155cb8e64ad0 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryLoadingException.java @@ -0,0 +1,15 @@ +package io.papermc.paper.plugin.loader.library; + +/** + * Indicates that an exception has occured while loading a library. + */ +public class LibraryLoadingException extends RuntimeException { + + public LibraryLoadingException(String s) { + super(s); + } + + public LibraryLoadingException(String s, Exception e) { + super(s, e); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/LibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryStore.java new file mode 100644 index 0000000000000000000000000000000000000000..0546fa1e9dcd7155086a8650806a8c086b6fc458 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryStore.java @@ -0,0 +1,26 @@ +package io.papermc.paper.plugin.loader.library; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Represents a storage that stores library jars. + *

+ * The library store api allows plugins to register specific dependencies into their runtime classloader when their + * {@link io.papermc.paper.plugin.loader.PluginLoader} is processed. + * + * @see io.papermc.paper.plugin.loader.PluginLoader + */ +@ApiStatus.Internal +public interface LibraryStore { + + /** + * Adds the provided library path to this library store. + * + * @param library path to the libraries jar file on the disk + */ + void addLibrary(@NotNull Path library); + +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/impl/JarLibrary.java b/src/main/java/io/papermc/paper/plugin/loader/library/impl/JarLibrary.java new file mode 100644 index 0000000000000000000000000000000000000000..e3da0d67cab01e1233dccde1a12ff25961ee79fb --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/library/impl/JarLibrary.java @@ -0,0 +1,45 @@ +package io.papermc.paper.plugin.loader.library.impl; + +import io.papermc.paper.plugin.loader.library.ClassPathLibrary; +import io.papermc.paper.plugin.loader.library.LibraryLoadingException; +import io.papermc.paper.plugin.loader.library.LibraryStore; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * A simple jar library implementation of the {@link ClassPathLibrary} that allows {@link io.papermc.paper.plugin.loader.PluginLoader}s to + * append a jar stored on the local file system into their runtime classloader. + *

+ * An example creation of the jar library type may look like this: + *

{@code
+ *   final JarLibrary customLibrary = new JarLibrary(Path.of("libs/custom-library-1.24.jar"));
+ * }
+ * resulting in a jar library that provides the jar at {@code libs/custom-library-1.24.jar} to the plugins classloader + * at runtime. + *

+ * The jar library implementation will error if the file does not exist at the specified path. + */ +public class JarLibrary implements ClassPathLibrary { + + private final Path path; + + /** + * Creates a new jar library that references the jar file found at the provided path. + * + * @param path the path, relative to the JVMs start directory. + */ + public JarLibrary(@NotNull Path path) { + this.path = path; + } + + @Override + public void register(@NotNull LibraryStore store) throws LibraryLoadingException { + if (Files.notExists(this.path)) { + throw new LibraryLoadingException("Could not find library at " + this.path); + } + + store.addLibrary(this.path); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/impl/MavenLibraryResolver.java b/src/main/java/io/papermc/paper/plugin/loader/library/impl/MavenLibraryResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..9af07d168beadaa77e4965819200eeb94fe3e092 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/library/impl/MavenLibraryResolver.java @@ -0,0 +1,132 @@ +package io.papermc.paper.plugin.loader.library.impl; + +import io.papermc.paper.plugin.loader.library.ClassPathLibrary; +import io.papermc.paper.plugin.loader.library.LibraryLoadingException; +import io.papermc.paper.plugin.loader.library.LibraryStore; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * The maven library resolver acts as a resolver for yet to be resolved jar libraries that may be pulled from a + * remote maven repository. + *

+ * Plugins may create and configure a {@link MavenLibraryResolver} by creating a new one and registering both + * a dependency artifact that should be resolved to a library at runtime and the repository it is found in. + * An example of this would be the inclusion of the jooq library for typesafe SQL queries: + *

{@code
+ * MavenLibraryResolver resolver = new MavenLibraryResolver();
+ * resolver.addDependency(new Dependency(new DefaultArtifact("org.jooq:jooq:3.17.7"), null));
+ * resolver.addRepository(new RemoteRepository.Builder(
+ *     "central", "default", "https://repo1.maven.org/maven2/"
+ * ).build());
+ * }
+ * + * Plugins may create and register a {@link MavenLibraryResolver} after configuring it. + */ +public class MavenLibraryResolver implements ClassPathLibrary { + + private static final Logger logger = LoggerFactory.getLogger("MavenLibraryResolver"); + + private final RepositorySystem repository; + private final DefaultRepositorySystemSession session; + private final List repositories = new ArrayList<>(); + private final List dependencies = new ArrayList<>(); + + /** + * Creates a new maven library resolver instance. + *

+ * The created instance will use the servers {@code libraries} folder to cache fetched libraries in. + * Notably, the resolver is created without any repository, not even maven central. + * It is hence crucial that plugins which aim to use this api register all required repositories before + * submitting the {@link MavenLibraryResolver} to the {@link io.papermc.paper.plugin.loader.PluginClasspathBuilder}. + */ + public MavenLibraryResolver() { + 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(); + + this.session.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_FAIL); + this.session.setLocalRepositoryManager(this.repository.newLocalRepositoryManager(this.session, new LocalRepository("libraries"))); + this.session.setTransferListener(new AbstractTransferListener() { + @Override + public void transferInitiated(@NotNull TransferEvent event) throws TransferCancelledException { + logger.info("Downloading {}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName()); + } + }); + this.session.setReadOnly(); + } + + /** + * Adds the provided dependency to the library resolver. + * The artifact from the first valid repository matching the passed dependency will be chosen. + * + * @param dependency the definition of the dependency the maven library resolver should resolve when running + * @see MavenLibraryResolver#addRepository(RemoteRepository) + */ + public void addDependency(@NotNull Dependency dependency) { + this.dependencies.add(dependency); + } + + /** + * Adds the provided repository to the library resolver. + * The order in which these are added does matter, as dependency resolving will start at the first added + * repository. + * + * @param remoteRepository the configuration that defines the maven repository this library resolver should fetch + * dependencies from + */ + public void addRepository(@NotNull RemoteRepository remoteRepository) { + this.repositories.add(remoteRepository); + } + + /** + * Resolves the provided dependencies and adds them to the library store. + * + * @param store the library store the then resolved and downloaded dependencies are registered into + * @throws LibraryLoadingException if resolving a dependency failed + */ + @Override + public void register(@NotNull LibraryStore store) throws LibraryLoadingException { + List repos = this.repository.newResolutionRepositories(this.session, this.repositories); + + DependencyResult result; + try { + result = this.repository.resolveDependencies(this.session, new DependencyRequest(new CollectRequest((Dependency) null, this.dependencies, repos), null)); + } catch (DependencyResolutionException ex) { + throw new LibraryLoadingException("Error resolving libraries", ex); + } + + for (ArtifactResult artifact : result.getArtifactResults()) { + File file = artifact.getArtifact().getFile(); + store.addLibrary(file.toPath()); + } + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/ClassLoaderAccess.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/ClassLoaderAccess.java new file mode 100644 index 0000000000000000000000000000000000000000..64e46fdfa4d404cb08c67a456e5990b729296b98 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/ClassLoaderAccess.java @@ -0,0 +1,34 @@ +package io.papermc.paper.plugin.provider.classloader; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * The class loader access interface is an internal representation of a class accesses' ability to see types + * from other {@link ConfiguredPluginClassLoader}. + *

+ * An example of this would be a class loader access representing a plugin. The class loader access in that case would + * only return {@code true} on calls for {@link #canAccess(ConfiguredPluginClassLoader)} if the passed class loader + * is owned by a direct or transitive dependency of the plugin, preventing the plugin for accidentally discovering and + * using class types that are supplied by plugins/libraries the plugin did not actively define as a dependency. + */ +@ApiStatus.Internal +public interface ClassLoaderAccess { + + /** + * Evaluates if this class loader access is allowed to access types provided by the passed {@link + * ConfiguredPluginClassLoader}. + *

+ * This interface method does not offer any further contracts on the interface level, as the logic to determine + * what class loaders this class loader access is allowed to retrieve types from depends heavily on the type of + * access. + * Legacy spigot types for example may access any class loader available on the server, while modern paper plugins + * are properly limited to their dependency tree. + * + * @param classLoader the class loader for which access should be evaluated + * @return a plain boolean flag, {@code true} indicating that this class loader access is allowed to access types + * from the passed configured plugin class loader, {@code false} indicating otherwise. + */ + boolean canAccess(ConfiguredPluginClassLoader classLoader); + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/ConfiguredPluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/ConfiguredPluginClassLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..a21bdc57564aef7caf43dde3b2bcb2fc7f30461c --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/ConfiguredPluginClassLoader.java @@ -0,0 +1,71 @@ +package io.papermc.paper.plugin.provider.classloader; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.io.Closeable; +import org.jetbrains.annotations.Nullable; + +/** + * The configured plugin class loader represents an internal abstraction over the classloaders used by the server + * to load and access a plugins classes during runtime. + *

+ * It implements {@link Closeable} to define the ability to shutdown and close the classloader that implements this + * interface. + */ +@ApiStatus.Internal +public interface ConfiguredPluginClassLoader extends Closeable { + + /** + * Provides the configuration of the plugin that this plugin classloader provides type access to. + * + * @return the plugin meta instance, holding all meta information about the plugin instance. + */ + PluginMeta getConfiguration(); + + /** + * Attempts to load a class from this plugin class loader using the passed fully qualified name. + * This lookup logic can be configured through the following parameters to define how wide or how narrow the + * class lookup should be. + * + * @param name the fully qualified name of the class to load + * @param resolve whether the class should be resolved if needed or not + * @param checkGlobal whether this lookup should check transitive dependencies, including either the legacy spigot + * global class loader or the paper {@link PluginClassLoaderGroup} + * @param checkLibraries whether the defined libraries should be checked for the class or not + * @return the class found at the fully qualified class name passed under the passed restrictions + * @throws ClassNotFoundException if the class could not be found considering the passed restrictions + * @see ClassLoader#loadClass(String) + * @see Class#forName(String, boolean, ClassLoader) + */ + Class loadClass(@NotNull String name, + boolean resolve, + boolean checkGlobal, + boolean checkLibraries) throws ClassNotFoundException; + + /** + * Initializes both this configured plugin class loader and the java plugin passed to link to each other. + * This logic is to be called exactly once when the initial setup between the class loader and the instantiated + * {@link JavaPlugin} is loaded. + * + * @param plugin the {@link JavaPlugin} that should be interlinked with this class loader. + */ + void init(JavaPlugin plugin); + + /** + * Gets the plugin held by this class loader. + * + * @return the plugin or null if it doesn't exist yet + */ + @Nullable JavaPlugin getPlugin(); + + /** + * Get the plugin classloader group + * that is used by the underlying classloader + * @return classloader + */ + @Nullable + PluginClassLoaderGroup getGroup(); +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorage.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..fefc87d5ffd36b848a7adb326a3a741e9edb28df --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorage.java @@ -0,0 +1,92 @@ +package io.papermc.paper.plugin.provider.classloader; + +import org.bukkit.plugin.java.PluginClassLoader; +import org.jetbrains.annotations.ApiStatus; + +/** + * The plugin classloader storage is an internal type that is used to manage existing classloaders on the server. + *

+ * The paper classloader storage is also responsible for storing added {@link ConfiguredPluginClassLoader}s into + * {@link PluginClassLoaderGroup}s, via {@link #registerOpenGroup(ConfiguredPluginClassLoader)}, + * {@link #registerSpigotGroup(PluginClassLoader)} and {@link + * #registerAccessBackedGroup(ConfiguredPluginClassLoader, ClassLoaderAccess)}. + *

+ * Groups are differentiated into the global group or plugin owned groups. + *

+ */ +@ApiStatus.Internal +public interface PaperClassLoaderStorage { + + /** + * Access to the shared instance of the {@link PaperClassLoaderStorageAccess}. + * + * @return the singleton instance of the {@link PaperClassLoaderStorage} used throughout the server + */ + static PaperClassLoaderStorage instance() { + return PaperClassLoaderStorageAccess.INSTANCE; + } + + /** + * Registers a legacy spigot {@link PluginClassLoader} into the loader storage, creating a group wrapping + * the single plugin class loader with transitive access to the global group. + * + * @param pluginClassLoader the legacy spigot plugin class loader to register + * @return the group the plugin class loader was placed into + */ + PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader); + + /** + * Registers a paper configured plugin classloader into a new open group, with full access to the global + * plugin class loader group. + *

+ * This method hence allows the configured plugin class loader to access all other class loaders registered in this + * storage. + * + * @param classLoader the configured plugin class loader to register + * @return the group the plugin class loader was placed into + */ + PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader); + + /** + * Registers a paper configured classloader into a new, access backed group. + * The access backed classloader group, different from an open group, only has access to the classloaders + * the passed {@link ClassLoaderAccess} grants access to. + * + * @param classLoader the configured plugin class loader to register + * @param access the class loader access that defines what other classloaders the passed plugin class loader + * should be granted access to. + * @return the group the plugin class loader was placed into. + */ + PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access); + + /** + * Unregisters a configured class loader from this storage. + * This removes the passed class loaders from any group it may have been a part of, including the global group. + *

+ * Note: this method is highly discouraged from being used, as mutation of the classloaders at runtime + * is not encouraged + * + * @param configuredPluginClassLoader the class loader to remove from this storage. + */ + void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader); + + /** + * Registers a configured plugin class loader directly into the global group without adding it to + * any existing groups. + *

+ * Note: this method unsafely injects the plugin classloader directly into the global group, which bypasses the + * group structure paper's plugin API introduced. This method should hence be used with caution. + * + * @param pluginLoader the configured plugin classloader instance that should be registered directly into the global + * group. + * @return a simple boolean flag, {@code true} if the classloader was registered or {@code false} if the classloader + * was already part of the global group. + */ + boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader); + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorageAccess.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorageAccess.java new file mode 100644 index 0000000000000000000000000000000000000000..2c0e5ba6f8eba7a632180491843071b8a8558e56 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorageAccess.java @@ -0,0 +1,17 @@ +package io.papermc.paper.plugin.provider.classloader; + +import net.kyori.adventure.util.Services; + +/** + * The paper classloader storage access acts as the holder for the server provided implementation of the + * {@link PaperClassLoaderStorage} interface. + */ +class PaperClassLoaderStorageAccess { + + /** + * The shared instance of the {@link PaperClassLoaderStorage}, supplied through the {@link java.util.ServiceLoader} + * by the server. + */ + static final PaperClassLoaderStorage INSTANCE = Services.service(PaperClassLoaderStorage.class).orElseThrow(); + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/PluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/PluginClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..885151cb932d9b8c09a7887edc879e154225f416 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PluginClassLoaderGroup.java @@ -0,0 +1,65 @@ +package io.papermc.paper.plugin.provider.classloader; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; + +/** + * A plugin classloader group represents a group of classloaders that a plugins classloader may access. + *

+ * An example of this would be a classloader group that holds all direct and transitive dependencies a plugin declared, + * allowing a plugins classloader to access classes included in these dependencies via this group. + */ +@ApiStatus.Internal +public interface PluginClassLoaderGroup { + + /** + * Attempts to find/load a class from this plugin class loader group using the passed fully qualified name + * in any of the classloaders that are part of this group. + *

+ * The lookup order across the contained loaders is not defined on the API level and depends purely on the + * implementation. + * + * @param name the fully qualified name of the class to load + * @param resolve whether the class should be resolved if needed or not + * @param requester plugin classloader that is requesting the class from this loader group + * @return the class found at the fully qualified class name passed. If the class could not be found, {@code null} + * will be returned. + * @see ConfiguredPluginClassLoader#loadClass(String, boolean, boolean, boolean) + */ + @Nullable + Class getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester); + + /** + * Removes a configured plugin classloader from this class loader group. + * If the classloader is not currently in the list, this method will simply do nothing. + * + * @param configuredPluginClassLoader the plugin classloader to remove from the group + */ + @Contract(mutates = "this") + void remove(ConfiguredPluginClassLoader configuredPluginClassLoader); + + /** + * Adds the passed plugin classloader to this group, allowing this group to use it during + * {@link #getClassByName(String, boolean, ConfiguredPluginClassLoader)} lookups. + *

+ * This method does not query the {@link ClassLoaderAccess} (exposed via {@link #getAccess()}) to ensure + * if this group has access to the class loader passed. + * + * @param configuredPluginClassLoader the plugin classloader to add to this group. + */ + @Contract(mutates = "this") + void add(ConfiguredPluginClassLoader configuredPluginClassLoader); + + /** + * Provides the class loader access that guards and defines the content of this classloader group. + * While not guaranteed contractually (see {@link #add(ConfiguredPluginClassLoader)}), the access generally is + * responsible for defining which {@link ConfiguredPluginClassLoader}s should be part of this group and which ones + * should not. + * + * @return the classloader access governing which classloaders should be part of this group and which ones should + * not. + */ + ClassLoaderAccess getAccess(); + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/entrypoint/DependencyContext.java b/src/main/java/io/papermc/paper/plugin/provider/entrypoint/DependencyContext.java new file mode 100644 index 0000000000000000000000000000000000000000..44d630c3eb2670c36134b9907519dc986b3761b4 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/entrypoint/DependencyContext.java @@ -0,0 +1,48 @@ +package io.papermc.paper.plugin.provider.entrypoint; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A dependency context is a read-only abstraction of a type/concept that can resolve dependencies between plugins. + *

+ * This may for example be the server wide plugin manager itself, capable of validating if a dependency exists between + * two {@link PluginMeta} instances, however the implementation is not limited to such a concrete use-case. + */ +@ApiStatus.Internal +public interface DependencyContext { + + /** + * Computes if the passed {@link PluginMeta} defined the passed dependency as a transitive dependency. + * A transitive dependency, as implied by its name, may not have been configured directly by the passed plugin + * but could also simply be a dependency of a dependency. + *

+ * A simple example of this method would be + *

{@code
+     * dependencyContext.isTransitiveDependency(pluginMetaA, pluginMetaC);
+     * }
+ * which would return {@code true} if {@code pluginMetaA} directly or indirectly depends on {@code pluginMetaC}. + * + * @param plugin the plugin meta this computation should consider the requester of the dependency status for the + * passed potential dependency. + * @param depend the potential transitive dependency of the {@code plugin} parameter. + * @return a simple boolean flag indicating if {@code plugin} considers {@code depend} as a transitive dependency. + */ + boolean isTransitiveDependency(@NotNull PluginMeta plugin, @NotNull PluginMeta depend); + + /** + * Computes if this dependency context is aware of a dependency that provides/matches the passed identifier. + *

+ * A dependency in this methods context is any dependable artefact. It does not matter if anything actually depends + * on said artefact, its mere existence as a potential dependency is enough for this method to consider it a + * dependency. If this dependency context is hence aware of an artefact with the matching identifier, this + * method returns {@code true}. + * + * @param pluginIdentifier the unique identifier of the dependency with which to probe this dependency context. + * @return a plain boolean flag indicating if this dependency context is aware of a potential dependency with the + * passed identifier. + */ + boolean hasDependency(@NotNull String pluginIdentifier); + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java b/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..6bf3d212a6156ad9ab0e82d1ca0a04f83f6e4b83 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java @@ -0,0 +1,78 @@ +package io.papermc.paper.plugin.provider.util; + +import com.destroystokyo.paper.util.SneakyThrow; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An internal utility type that holds logic for loading a provider-like type from a classloaders. + * Provides, at least in the context of this utility, define themselves as implementations of a specific parent + * interface/type, e.g. {@link org.bukkit.plugin.java.JavaPlugin} and implement a no-args constructor. + */ +@ApiStatus.Internal +public class ProviderUtil { + + /** + * Loads the class found at the provided fully qualified class name from the passed classloader, creates a new + * instance of it using the no-args constructor, that should exist as per this method contract, and casts it to the + * provided parent type. + * + * @param clazz the fully qualified name of the class to load + * @param classType the parent type that the created object found at the {@code clazz} name should be cast to + * @param loader the loader from which the class should be loaded + * @param the generic type of the parent class the created object will be cast to + * @return the object instantiated from the class found at the provided FQN, cast to the parent type + */ + @NotNull + public static T loadClass(@NotNull String clazz, @NotNull Class classType, @NotNull ClassLoader loader) { + return loadClass(clazz, classType, loader, null); + } + + /** + * Loads the class found at the provided fully qualified class name from the passed classloader, creates a new + * instance of it using the no-args constructor, that should exist as per this method contract, and casts it to the + * provided parent type. + * + * @param clazz the fully qualified name of the class to load + * @param classType the parent type that the created object found at the {@code clazz} name should be cast to + * @param loader the loader from which the class should be loaded + * @param onError a runnable that is executed before any unknown exception is raised through a sneaky throw. + * @param the generic type of the parent class the created object will be cast to + * @return the object instantiated from the class found at the provided fully qualified class name, cast to the + * parent type + */ + @NotNull + public static T loadClass(@NotNull String clazz, @NotNull Class classType, @NotNull ClassLoader loader, @Nullable Runnable onError) { + try { + T clazzInstance; + + try { + Class jarClass = Class.forName(clazz, true, loader); + + Class pluginClass; + try { + pluginClass = jarClass.asSubclass(classType); + } catch (ClassCastException ex) { + throw new ClassCastException("class '%s' does not extend '%s'".formatted(clazz, classType)); + } + + clazzInstance = pluginClass.getDeclaredConstructor().newInstance(); + } catch (IllegalAccessException exception) { + throw new RuntimeException("No public constructor"); + } catch (InstantiationException exception) { + throw new RuntimeException("Abnormal class instantiation", exception); + } + + return clazzInstance; + } catch (Throwable e) { + if (onError != null) { + onError.run(); + } + SneakyThrow.sneaky(e); + } + + throw new AssertionError(); // Shouldn't happen + } + +} diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java index fd0ae07b8a19f6b628601a487329a929f3a26c91..2042e3fb0ea347148814d9838cd7bb475bd23984 100644 --- a/src/main/java/org/bukkit/UnsafeValues.java +++ b/src/main/java/org/bukkit/UnsafeValues.java @@ -101,4 +101,14 @@ public interface UnsafeValues { @Nullable FeatureFlag getFeatureFlag(@NotNull NamespacedKey key); + + // Paper start + @Deprecated(forRemoval = true) + boolean isSupportedApiVersion(String apiVersion); + + @Deprecated(forRemoval = true) + static boolean isLegacyPlugin(org.bukkit.plugin.Plugin plugin) { + return !Bukkit.getUnsafe().isSupportedApiVersion(plugin.getDescription().getAPIVersion()); + } + // Paper end } diff --git a/src/main/java/org/bukkit/command/PluginCommand.java b/src/main/java/org/bukkit/command/PluginCommand.java index 1dbbc244309043b18c1d71707c4fb066c0d0e02d..551c5af6a7bfa2268cbc63be8e70d129bccaa912 100644 --- a/src/main/java/org/bukkit/command/PluginCommand.java +++ b/src/main/java/org/bukkit/command/PluginCommand.java @@ -14,7 +14,7 @@ public final class PluginCommand extends Command implements PluginIdentifiableCo private CommandExecutor executor; private TabCompleter completer; - protected PluginCommand(@NotNull String name, @NotNull Plugin owner) { + PluginCommand(@NotNull String name, @NotNull Plugin owner) { super(name); this.executor = owner; this.owningPlugin = owner; diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java index e195e74c48c69047aa825b75fad95419c505b41f..53f28c9e6843991486a576d41b6641c170589807 100644 --- a/src/main/java/org/bukkit/command/SimpleCommandMap.java +++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java @@ -34,7 +34,7 @@ public class SimpleCommandMap implements CommandMap { private void setDefaultCommands() { register("bukkit", new VersionCommand("version")); register("bukkit", new ReloadCommand("reload")); - register("bukkit", new PluginsCommand("plugins")); + //register("bukkit", new PluginsCommand("plugins")); // Paper register("bukkit", new TimingsCommand("timings")); } diff --git a/src/main/java/org/bukkit/command/defaults/PluginsCommand.java b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java index bcb576a4271b1ec7b1cfe6f83cf161b7d89ed2e5..4d849e1283fdf6b0872a0f2183964cc93748c116 100644 --- a/src/main/java/org/bukkit/command/defaults/PluginsCommand.java +++ b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java @@ -9,6 +9,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +@Deprecated(forRemoval = true) // Paper public class PluginsCommand extends BukkitCommand { public PluginsCommand(@NotNull String name) { super(name); diff --git a/src/main/java/org/bukkit/plugin/Plugin.java b/src/main/java/org/bukkit/plugin/Plugin.java index b37938745f916b5f0111b07b1a1c97527f026e9d..8c76716249e44ed8bf6be94c1f5c7b6d9bb35be2 100644 --- a/src/main/java/org/bukkit/plugin/Plugin.java +++ b/src/main/java/org/bukkit/plugin/Plugin.java @@ -30,10 +30,21 @@ public interface Plugin extends TabExecutor { * Returns the plugin.yaml file containing the details for this plugin * * @return Contents of the plugin.yaml file + * @deprecated May be inaccurate due to different plugin implementations. + * @see Plugin#getPluginMeta() */ + @Deprecated // Paper @NotNull public PluginDescriptionFile getDescription(); + // Paper start + /** + * Gets the plugin meta for this plugin. + * @return configuration + */ + @NotNull + io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta(); + // Paper end /** * Gets a {@link FileConfiguration} for this plugin, read through * "config.yml" @@ -94,6 +105,7 @@ public interface Plugin extends TabExecutor { * * @return PluginLoader that controls this plugin */ + @Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future @NotNull public PluginLoader getPluginLoader(); diff --git a/src/main/java/org/bukkit/plugin/PluginBase.java b/src/main/java/org/bukkit/plugin/PluginBase.java index 94f8ceb965cecb5669a84a0ec61c0f706c2a2673..e773db6da357ad210eb24d4c389af2dc84ce450a 100644 --- a/src/main/java/org/bukkit/plugin/PluginBase.java +++ b/src/main/java/org/bukkit/plugin/PluginBase.java @@ -31,6 +31,6 @@ public abstract class PluginBase implements Plugin { @Override @NotNull public final String getName() { - return getDescription().getName(); + return getPluginMeta().getName(); // Paper } } diff --git a/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java b/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java index 0eae1c027cab0444a125ae4efed7f9fcb1b9934f..07e8908d25fcd4e5eabadc9f019b54acff3b5e3c 100644 --- a/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java +++ b/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java @@ -197,7 +197,7 @@ import org.yaml.snakeyaml.representer.Representer; * inferno.burningdeaths: true * */ -public final class PluginDescriptionFile { +public final class PluginDescriptionFile implements io.papermc.paper.plugin.configuration.PluginMeta { // Paper private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z0-9 _.-]+$"); private static final ThreadLocal YAML = new ThreadLocal() { @Override @@ -258,6 +258,70 @@ public final class PluginDescriptionFile { private Set awareness = ImmutableSet.of(); private String apiVersion = null; private List libraries = ImmutableList.of(); + // Paper start - oh my goddddd + /** + * Don't use this. + */ + @org.jetbrains.annotations.ApiStatus.Internal + public PluginDescriptionFile(String rawName, String name, List provides, String main, String classLoaderOf, List depend, List softDepend, List loadBefore, String version, Map> commands, String description, List authors, List contributors, String website, String prefix, PluginLoadOrder order, List permissions, PermissionDefault defaultPerm, Set awareness, String apiVersion, List libraries) { + this.rawName = rawName; + this.name = name; + this.provides = provides; + this.main = main; + this.classLoaderOf = classLoaderOf; + this.depend = depend; + this.softDepend = softDepend; + this.loadBefore = loadBefore; + this.version = version; + this.commands = commands; + this.description = description; + this.authors = authors; + this.contributors = contributors; + this.website = website; + this.prefix = prefix; + this.order = order; + this.permissions = permissions; + this.defaultPerm = defaultPerm; + this.awareness = awareness; + this.apiVersion = apiVersion; + this.libraries = libraries; + } + + @Override + public @NotNull String getMainClass() { + return this.main; + } + + @Override + public @NotNull PluginLoadOrder getLoadOrder() { + return this.order; + } + + @Override + public @Nullable String getLoggerPrefix() { + return this.prefix; + } + + @Override + public @NotNull List getPluginDependencies() { + return this.depend; + } + + @Override + public @NotNull List getPluginSoftDependencies() { + return this.softDepend; + } + + @Override + public @NotNull List getLoadBeforePlugins() { + return this.loadBefore; + } + + @Override + public @NotNull List getProvidedPlugins() { + return this.provides; + } + // Paper end public PluginDescriptionFile(@NotNull final InputStream stream) throws InvalidDescriptionException { loadMap(asMap(YAML.get().load(stream))); diff --git a/src/main/java/org/bukkit/plugin/PluginLoader.java b/src/main/java/org/bukkit/plugin/PluginLoader.java index a88733f1cd1ddb5d85ab1b0e6af4fd5b80bbc1c6..cb530369e667c426c842da356c31304bb5c3ecfa 100644 --- a/src/main/java/org/bukkit/plugin/PluginLoader.java +++ b/src/main/java/org/bukkit/plugin/PluginLoader.java @@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull; * Represents a plugin loader, which handles direct access to specific types * of plugins */ +@Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future public interface PluginLoader { /** diff --git a/src/main/java/org/bukkit/plugin/PluginManager.java b/src/main/java/org/bukkit/plugin/PluginManager.java index 41e26451fe12d8e6e0ef73c85731b24b4e3f200c..03213fde8315384ec56c16031cfc606ade2e8091 100644 --- a/src/main/java/org/bukkit/plugin/PluginManager.java +++ b/src/main/java/org/bukkit/plugin/PluginManager.java @@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable; /** * Handles all plugin management from the Server */ -public interface PluginManager { +public interface PluginManager extends io.papermc.paper.plugin.PermissionManager { // Paper /** * Registers the specified plugin loader @@ -23,6 +23,7 @@ public interface PluginManager { * @throws IllegalArgumentException Thrown when the given Class is not a * valid PluginLoader */ + @Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future public void registerInterface(@NotNull Class loader) throws IllegalArgumentException; /** @@ -303,4 +304,17 @@ public interface PluginManager { * @return True if event timings are to be used */ public boolean useTimings(); + + // Paper start + @org.jetbrains.annotations.ApiStatus.Internal + boolean isTransitiveDependency(io.papermc.paper.plugin.configuration.PluginMeta pluginMeta, io.papermc.paper.plugin.configuration.PluginMeta dependencyConfig); + + /** + * Sets the permission manager to be used for this server. + * + * @param permissionManager permission manager + */ + @org.jetbrains.annotations.ApiStatus.Experimental + void overridePermissionManager(@NotNull Plugin plugin, @Nullable io.papermc.paper.plugin.PermissionManager permissionManager); + // Paper end } diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java index a69c5d5cad6168aeaae41e8adc319dc8c976b1e2..77f9ebbe675cf1b6a17e98d98e7666711998eb4e 100644 --- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java @@ -43,6 +43,8 @@ import org.jetbrains.annotations.Nullable; /** * Handles all plugin management from the Server */ +@Deprecated(forRemoval = true) // Paper - This implementation may be replaced in a future version of Paper. +// Plugins may still reflect into this class to modify permission logic for the time being. public final class SimplePluginManager implements PluginManager { private final Server server; private final Map fileAssociations = new HashMap(); @@ -51,10 +53,13 @@ public final class SimplePluginManager implements PluginManager { private MutableGraph dependencyGraph = GraphBuilder.directed().build(); private File updateDirectory; private final SimpleCommandMap commandMap; - private final Map permissions = new HashMap(); - private final Map> defaultPerms = new LinkedHashMap>(); - private final Map> permSubs = new HashMap>(); - private final Map> defSubs = new HashMap>(); + // Paper start + public final Map permissions = new HashMap(); + public final Map> defaultPerms = new LinkedHashMap>(); + public final Map> permSubs = new HashMap>(); + public final Map> defSubs = new HashMap>(); + public PluginManager paperPluginManager; + // Paper end private boolean useTimings = false; public SimplePluginManager(@NotNull Server instance, @NotNull SimpleCommandMap commandMap) { @@ -111,6 +116,11 @@ public final class SimplePluginManager implements PluginManager { @Override @NotNull public Plugin[] loadPlugins(@NotNull File directory) { + if (true) { + List pluginList = new ArrayList<>(); + java.util.Collections.addAll(pluginList, this.paperPluginManager.loadPlugins(directory)); + return pluginList.toArray(new Plugin[0]); + } Preconditions.checkArgument(directory != null, "Directory cannot be null"); Preconditions.checkArgument(directory.isDirectory(), "Directory must be a directory"); @@ -378,6 +388,15 @@ public final class SimplePluginManager implements PluginManager { @Nullable public synchronized Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException { Preconditions.checkArgument(file != null, "File cannot be null"); + // Paper start + if (true) { + try { + return this.paperPluginManager.loadPlugin(file); + } catch (org.bukkit.plugin.InvalidDescriptionException ignored) { + return null; + } + } + // Paper end checkUpdate(file); @@ -428,12 +447,14 @@ public final class SimplePluginManager implements PluginManager { @Override @Nullable public synchronized Plugin getPlugin(@NotNull String name) { + if (true) {return this.paperPluginManager.getPlugin(name);} // Paper return lookupNames.get(name.replace(' ', '_')); } @Override @NotNull public synchronized Plugin[] getPlugins() { + if (true) {return this.paperPluginManager.getPlugins();} // Paper return plugins.toArray(new Plugin[plugins.size()]); } @@ -447,6 +468,7 @@ public final class SimplePluginManager implements PluginManager { */ @Override public boolean isPluginEnabled(@NotNull String name) { + if (true) {return this.paperPluginManager.isPluginEnabled(name);} // Paper Plugin plugin = getPlugin(name); return isPluginEnabled(plugin); @@ -460,6 +482,7 @@ public final class SimplePluginManager implements PluginManager { */ @Override public boolean isPluginEnabled(@Nullable Plugin plugin) { + if (true) {return this.paperPluginManager.isPluginEnabled(plugin);} // Paper if ((plugin != null) && (plugins.contains(plugin))) { return plugin.isEnabled(); } else { @@ -469,6 +492,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void enablePlugin(@NotNull final Plugin plugin) { + if (true) {this.paperPluginManager.enablePlugin(plugin); return;} // Paper if (!plugin.isEnabled()) { List pluginCommands = PluginCommandYamlParser.parse(plugin); @@ -488,6 +512,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void disablePlugins() { + if (true) {this.paperPluginManager.disablePlugins(); return;} // Paper Plugin[] plugins = getPlugins(); for (int i = plugins.length - 1; i >= 0; i--) { disablePlugin(plugins[i]); @@ -496,6 +521,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void disablePlugin(@NotNull final Plugin plugin) { + if (true) {this.paperPluginManager.disablePlugin(plugin); return;} // Paper if (plugin.isEnabled()) { try { plugin.getPluginLoader().disablePlugin(plugin); @@ -540,6 +566,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void clearPlugins() { + if (true) {this.paperPluginManager.clearPlugins(); return;} // Paper synchronized (this) { disablePlugins(); plugins.clear(); @@ -560,6 +587,7 @@ public final class SimplePluginManager implements PluginManager { */ @Override public void callEvent(@NotNull Event event) { + if (true) {this.paperPluginManager.callEvent(event); return;} // Paper if (event.isAsynchronous()) { if (Thread.holdsLock(this)) { throw new IllegalStateException(event.getEventName() + " cannot be triggered asynchronously from inside synchronized code."); @@ -608,6 +636,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) { + if (true) {this.paperPluginManager.registerEvents(listener, plugin); return;} // Paper if (!plugin.isEnabled()) { throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled"); } @@ -641,6 +670,7 @@ public final class SimplePluginManager implements PluginManager { Preconditions.checkArgument(priority != null, "Priority cannot be null"); Preconditions.checkArgument(executor != null, "Executor cannot be null"); Preconditions.checkArgument(plugin != null, "Plugin cannot be null"); + if (true) {this.paperPluginManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled); return;} // Paper if (!plugin.isEnabled()) { throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled"); @@ -688,16 +718,19 @@ public final class SimplePluginManager implements PluginManager { @Override @Nullable public Permission getPermission(@NotNull String name) { + if (true) {return this.paperPluginManager.getPermission(name);} // Paper return permissions.get(name.toLowerCase(java.util.Locale.ENGLISH)); } @Override public void addPermission(@NotNull Permission perm) { + if (true) {this.paperPluginManager.addPermission(perm); return;} // Paper addPermission(perm, true); } @Deprecated public void addPermission(@NotNull Permission perm, boolean dirty) { + if (true) {this.paperPluginManager.addPermission(perm); return;} // Paper - This just has a performance implication, use the better api to avoid this. String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH); if (permissions.containsKey(name)) { @@ -711,21 +744,25 @@ public final class SimplePluginManager implements PluginManager { @Override @NotNull public Set getDefaultPermissions(boolean op) { + if (true) {return this.paperPluginManager.getDefaultPermissions(op);} // Paper return ImmutableSet.copyOf(defaultPerms.get(op)); } @Override public void removePermission(@NotNull Permission perm) { + if (true) {this.paperPluginManager.removePermission(perm); return;} // Paper removePermission(perm.getName()); } @Override public void removePermission(@NotNull String name) { + if (true) {this.paperPluginManager.removePermission(name); return;} // Paper permissions.remove(name.toLowerCase(java.util.Locale.ENGLISH)); } @Override public void recalculatePermissionDefaults(@NotNull Permission perm) { + if (true) {this.paperPluginManager.recalculatePermissionDefaults(perm); return;} // Paper if (perm != null && permissions.containsKey(perm.getName().toLowerCase(java.util.Locale.ENGLISH))) { defaultPerms.get(true).remove(perm); defaultPerms.get(false).remove(perm); @@ -765,6 +802,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) { + if (true) {this.paperPluginManager.subscribeToPermission(permission, permissible); return;} // Paper String name = permission.toLowerCase(java.util.Locale.ENGLISH); Map map = permSubs.get(name); @@ -778,6 +816,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) { + if (true) {this.paperPluginManager.unsubscribeFromPermission(permission, permissible); return;} // Paper String name = permission.toLowerCase(java.util.Locale.ENGLISH); Map map = permSubs.get(name); @@ -793,6 +832,7 @@ public final class SimplePluginManager implements PluginManager { @Override @NotNull public Set getPermissionSubscriptions(@NotNull String permission) { + if (true) {return this.paperPluginManager.getPermissionSubscriptions(permission);} // Paper String name = permission.toLowerCase(java.util.Locale.ENGLISH); Map map = permSubs.get(name); @@ -805,6 +845,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) { + if (true) {this.paperPluginManager.subscribeToDefaultPerms(op, permissible); return;} // Paper Map map = defSubs.get(op); if (map == null) { @@ -817,6 +858,7 @@ public final class SimplePluginManager implements PluginManager { @Override public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) { + if (true) {this.paperPluginManager.unsubscribeFromDefaultPerms(op, permissible); return;} // Paper Map map = defSubs.get(op); if (map != null) { @@ -831,6 +873,7 @@ public final class SimplePluginManager implements PluginManager { @Override @NotNull public Set getDefaultPermSubscriptions(boolean op) { + if (true) {return this.paperPluginManager.getDefaultPermSubscriptions(op);} // Paper Map map = defSubs.get(op); if (map == null) { @@ -843,6 +886,7 @@ public final class SimplePluginManager implements PluginManager { @Override @NotNull public Set getPermissions() { + if (true) {return this.paperPluginManager.getPermissions();} // Paper return new HashSet(permissions.values()); } @@ -866,6 +910,7 @@ public final class SimplePluginManager implements PluginManager { @Override public boolean useTimings() { + if (true) {return this.paperPluginManager.useTimings();} // Paper return useTimings; } @@ -877,4 +922,28 @@ public final class SimplePluginManager implements PluginManager { public void useTimings(boolean use) { useTimings = use; } + + // Paper start + public void clearPermissions() { + if (true) {this.paperPluginManager.clearPermissions(); return;} // Paper + permissions.clear(); + defaultPerms.get(true).clear(); + defaultPerms.get(false).clear(); + } + + @Override + public boolean isTransitiveDependency(io.papermc.paper.plugin.configuration.PluginMeta pluginMeta, io.papermc.paper.plugin.configuration.PluginMeta dependencyConfig) { + return this.paperPluginManager.isTransitiveDependency(pluginMeta, dependencyConfig); + } + + @Override + public void overridePermissionManager(@NotNull Plugin plugin, @Nullable io.papermc.paper.plugin.PermissionManager permissionManager) { + this.paperPluginManager.overridePermissionManager(plugin, permissionManager); + } + + @Override + public void addPermissions(@NotNull List perm) { + this.paperPluginManager.addPermissions(perm); + } + // Paper end } diff --git a/src/main/java/org/bukkit/plugin/UnknownDependencyException.java b/src/main/java/org/bukkit/plugin/UnknownDependencyException.java index a80251eff75430863b37db1c131e22593f3fcd5e..310c4041963a3f1e0a26e39a6da12a9bfdb51edc 100644 --- a/src/main/java/org/bukkit/plugin/UnknownDependencyException.java +++ b/src/main/java/org/bukkit/plugin/UnknownDependencyException.java @@ -43,4 +43,16 @@ public class UnknownDependencyException extends RuntimeException { public UnknownDependencyException() { } + // Paper start + /** + * Create a new {@link UnknownDependencyException} with a message informing + * about which dependencies are missing for what plugin. + * + * @param missingDependencies missing dependencies + * @param pluginName plugin which is missing said dependencies + */ + public UnknownDependencyException(final @org.jetbrains.annotations.NotNull java.util.Collection missingDependencies, final @org.jetbrains.annotations.NotNull String pluginName) { + this("Unknown/missing dependency plugins: [" + String.join(", ", missingDependencies) + "]. Please download and install these plugins to run '" + pluginName + "'."); + } + // Paper end } diff --git a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java index ee100b7ad89ce1eccef0c3bc55885cd78aadd1ec..f594913e6b94f77b26a4a758c447a42d8a25b6ff 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java @@ -40,6 +40,7 @@ public abstract class JavaPlugin extends PluginBase { private Server server = null; private File file = null; private PluginDescriptionFile description = null; + private io.papermc.paper.plugin.configuration.PluginMeta pluginMeta = null; // Paper private File dataFolder = null; private ClassLoader classLoader = null; private boolean naggable = true; @@ -48,13 +49,16 @@ public abstract class JavaPlugin extends PluginBase { private PluginLogger logger = null; public JavaPlugin() { - final ClassLoader classLoader = this.getClass().getClassLoader(); - if (!(classLoader instanceof PluginClassLoader)) { - throw new IllegalStateException("JavaPlugin requires " + PluginClassLoader.class.getName()); + // Paper start + if (this.getClass().getClassLoader() instanceof io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader configuredPluginClassLoader) { + configuredPluginClassLoader.init(this); + } else { + throw new IllegalStateException("JavaPlugin requires to be created by a valid classloader."); } - ((PluginClassLoader) classLoader).initialize(this); + // Paper end } + @Deprecated(forRemoval = true) // Paper protected JavaPlugin(@NotNull final JavaPluginLoader loader, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file) { final ClassLoader classLoader = this.getClass().getClassLoader(); if (classLoader instanceof PluginClassLoader) { @@ -79,9 +83,12 @@ public abstract class JavaPlugin extends PluginBase { * Gets the associated PluginLoader responsible for this plugin * * @return PluginLoader that controls this plugin + * @deprecated Plugin loading now occurs at a point which makes it impossible to expose this + * behavior. This instance will only throw unsupported operation exceptions. */ @NotNull @Override + @Deprecated(forRemoval = true) // Paper public final PluginLoader getPluginLoader() { return loader; } @@ -122,13 +129,20 @@ public abstract class JavaPlugin extends PluginBase { * Returns the plugin.yaml file containing the details for this plugin * * @return Contents of the plugin.yaml file + * @deprecated No longer applicable to all types of plugins */ @NotNull @Override + @Deprecated public final PluginDescriptionFile getDescription() { return description; } + @NotNull + public final io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { + return this.pluginMeta; + } + @NotNull @Override public FileConfiguration getConfig() { @@ -258,7 +272,8 @@ public abstract class JavaPlugin extends PluginBase { * * @param enabled true if enabled, otherwise false */ - protected final void setEnabled(final boolean enabled) { + @org.jetbrains.annotations.ApiStatus.Internal // Paper + public final void setEnabled(final boolean enabled) { // Paper if (isEnabled != enabled) { isEnabled = enabled; @@ -270,9 +285,18 @@ public abstract class JavaPlugin extends PluginBase { } } - - final void init(@NotNull PluginLoader loader, @NotNull Server server, @NotNull PluginDescriptionFile description, @NotNull File dataFolder, @NotNull File file, @NotNull ClassLoader classLoader) { - this.loader = loader; + // Paper start + private static class DummyPluginLoaderImplHolder { + private static final PluginLoader INSTANCE = net.kyori.adventure.util.Services.service(PluginLoader.class) + .orElseThrow(); + } + public final void init(@NotNull PluginLoader loader, @NotNull Server server, @NotNull PluginDescriptionFile description, @NotNull File dataFolder, @NotNull File file, @NotNull ClassLoader classLoader) { + init(server, description, dataFolder, file, classLoader, description); + this.pluginMeta = description; + } + public final void init(@NotNull Server server, @NotNull PluginDescriptionFile description, @NotNull File dataFolder, @NotNull File file, @NotNull ClassLoader classLoader, @Nullable io.papermc.paper.plugin.configuration.PluginMeta configuration) { + // Paper end + this.loader = DummyPluginLoaderImplHolder.INSTANCE; // Paper this.server = server; this.file = file; this.description = description; @@ -280,6 +304,7 @@ public abstract class JavaPlugin extends PluginBase { this.classLoader = classLoader; this.configFile = new File(dataFolder, "config.yml"); this.logger = new PluginLogger(this); + this.pluginMeta = configuration; // Paper } /** @@ -396,10 +421,10 @@ public abstract class JavaPlugin extends PluginBase { throw new IllegalArgumentException(clazz + " does not extend " + JavaPlugin.class); } final ClassLoader cl = clazz.getClassLoader(); - if (!(cl instanceof PluginClassLoader)) { - throw new IllegalArgumentException(clazz + " is not initialized by " + PluginClassLoader.class); + if (!(cl instanceof io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader configuredPluginClassLoader)) { // Paper + throw new IllegalArgumentException(clazz + " is not initialized by a " + io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader.class); // Paper } - JavaPlugin plugin = ((PluginClassLoader) cl).plugin; + JavaPlugin plugin = configuredPluginClassLoader.getPlugin(); // Paper if (plugin == null) { throw new IllegalStateException("Cannot get plugin for " + clazz + " from a static initializer"); } @@ -422,10 +447,10 @@ public abstract class JavaPlugin extends PluginBase { public static JavaPlugin getProvidingPlugin(@NotNull Class clazz) { Preconditions.checkArgument(clazz != null, "Null class cannot have a plugin"); final ClassLoader cl = clazz.getClassLoader(); - if (!(cl instanceof PluginClassLoader)) { - throw new IllegalArgumentException(clazz + " is not provided by " + PluginClassLoader.class); + if (!(cl instanceof io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader configuredPluginClassLoader)) { // Paper + throw new IllegalArgumentException(clazz + " is not provided by a " + io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader.class); // Paper } - JavaPlugin plugin = ((PluginClassLoader) cl).plugin; + JavaPlugin plugin = configuredPluginClassLoader.getPlugin(); // Paper if (plugin == null) { throw new IllegalStateException("Cannot get plugin for " + clazz + " from a static initializer"); } diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java index 047c0304fd617cec990f80815b43916c6ef5a94c..ab04ffe4cd05315a2ee0f64c553b4c674740eb7f 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -49,6 +49,7 @@ import org.yaml.snakeyaml.error.YAMLException; /** * Represents a Java plugin loader, allowing plugins in the form of .jar */ +@Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future. This implementation will be moved. public final class JavaPluginLoader implements PluginLoader { final Server server; private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")}; @@ -79,6 +80,7 @@ public final class JavaPluginLoader implements PluginLoader { @Override @NotNull public Plugin loadPlugin(@NotNull final File file) throws InvalidPluginException { + if (true) throw new UnsupportedOperationException(); // Paper Preconditions.checkArgument(file != null, "File cannot be null"); if (!file.exists()) { @@ -142,7 +144,7 @@ public final class JavaPluginLoader implements PluginLoader { final PluginClassLoader loader; try { - loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file, (libraryLoader != null) ? libraryLoader.createLoader(description) : null); + loader = new PluginClassLoader(getClass().getClassLoader(), description, dataFolder, file, (libraryLoader != null) ? libraryLoader.createLoader(description) : null, null, null); // Paper } catch (InvalidPluginException ex) { throw ex; } catch (Throwable ex) { diff --git a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java index 6d634b0ea813ccb19f1562a7d0e5a59cea4eab21..e4b6f278a811acbb0070e311c5c3bdaff7b00474 100644 --- a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java +++ b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java @@ -36,7 +36,10 @@ import org.eclipse.aether.transport.http.HttpTransporterFactory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -class LibraryLoader +// Paper start +@org.jetbrains.annotations.ApiStatus.Internal +public class LibraryLoader +// Paper end { private final Logger logger; @@ -79,7 +82,7 @@ class LibraryLoader } logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[] { - desc.getName(), desc.getLibraries().size() + java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), desc.getLibraries().size() // Paper - use configured log prefix } ); List dependencies = new ArrayList<>(); @@ -117,7 +120,7 @@ class LibraryLoader jarFiles.add( url ); logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[] { - desc.getName(), file + java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), file // Paper - use configured log prefix } ); } diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java index 2f74ec96ece706de23156ebabfe493211bc05391..302319acbc257a075adfb78d9f5c49fdadf45bdc 100644 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java @@ -29,7 +29,8 @@ import org.jetbrains.annotations.Nullable; /** * A ClassLoader for plugins, to allow shared classes across multiple plugins */ -final class PluginClassLoader extends URLClassLoader { +@org.jetbrains.annotations.ApiStatus.Internal // Paper +public final class PluginClassLoader extends URLClassLoader implements io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader { // Paper private final JavaPluginLoader loader; private final Map> classes = new ConcurrentHashMap>(); private final PluginDescriptionFile description; @@ -43,24 +44,32 @@ final class PluginClassLoader extends URLClassLoader { private JavaPlugin pluginInit; private IllegalStateException pluginState; private final Set seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private java.util.logging.Logger logger; // Paper - add field + private io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup classLoaderGroup; // Paper + public io.papermc.paper.plugin.provider.entrypoint.DependencyContext dependencyContext; // Paper static { ClassLoader.registerAsParallelCapable(); } - 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 { + @org.jetbrains.annotations.ApiStatus.Internal // Paper + public PluginClassLoader(@Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file, @Nullable ClassLoader libraryLoader, JarFile jarFile, io.papermc.paper.plugin.provider.entrypoint.DependencyContext dependencyContext) throws IOException, InvalidPluginException, MalformedURLException { // Paper // Paper - use JarFile provided by SpigotPluginProvider super(new URL[] {file.toURI().toURL()}, parent); - Preconditions.checkArgument(loader != null, "Loader cannot be null"); + this.loader = null; // Paper - pass null into loader field - this.loader = loader; this.description = description; this.dataFolder = dataFolder; this.file = file; - this.jar = new JarFile(file); + this.jar = jarFile; // Paper - use JarFile provided by SpigotPluginProvider this.manifest = jar.getManifest(); this.url = file.toURI().toURL(); this.libraryLoader = libraryLoader; + + // Paper start + this.dependencyContext = dependencyContext; + this.classLoaderGroup = io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage.instance().registerSpigotGroup(this); + // Paper end try { Class jarClass; try { @@ -94,6 +103,27 @@ final class PluginClassLoader extends URLClassLoader { return findResources(name); } + // Paper start + @Override + public Class loadClass(@NotNull String name, boolean resolve, boolean checkGlobal, boolean checkLibraries) throws ClassNotFoundException { + return this.loadClass0(name, resolve, checkGlobal, checkLibraries); + } + @Override + public io.papermc.paper.plugin.configuration.PluginMeta getConfiguration() { + return this.description; + } + + @Override + public void init(JavaPlugin plugin) { + this.initialize(plugin); + } + + @Override + public JavaPlugin getPlugin() { + return this.plugin; + } + // Paper end + @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { return loadClass0(name, resolve, true, true); @@ -119,26 +149,11 @@ final class PluginClassLoader extends URLClassLoader { if (checkGlobal) { // This ignores the libraries of other plugins, unless they are transitive dependencies. - Class result = loader.getClassByName(name, resolve, description); + Class result = this.classLoaderGroup.getClassByName(name, resolve, this); // Paper if (result != null) { // If the class was loaded from a library instead of a PluginClassLoader, we can assume that its associated plugin is a transitive dependency and can therefore skip this check. - if (result.getClassLoader() instanceof PluginClassLoader) { - 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 or softdepend 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 or softdepend of this plugin.", new Object[]{description.getName(), name, provider.getFullName()}); - } - } - } + // Paper - Totally delete the illegal access logic, we are never going to enforce it anyways here. return result; } @@ -167,7 +182,7 @@ final class PluginClassLoader extends URLClassLoader { throw new ClassNotFoundException(name, ex); } - classBytes = loader.server.getUnsafe().processClass(description, path, classBytes); + classBytes = org.bukkit.Bukkit.getServer().getUnsafe().processClass(description, path, classBytes); // Paper int dot = name.lastIndexOf('.'); if (dot != -1) { @@ -197,8 +212,8 @@ final class PluginClassLoader extends URLClassLoader { result = super.findClass(name); } - loader.setClass(name, result); classes.put(name, result); + this.setClass(name, result); // Paper } return result; @@ -207,6 +222,12 @@ final class PluginClassLoader extends URLClassLoader { @Override public void close() throws IOException { try { + // Paper start + Collection> classes = getClasses(); + for (Class clazz : classes) { + removeClass(clazz); + } + // Paper end super.close(); } finally { jar.close(); @@ -218,7 +239,7 @@ final class PluginClassLoader extends URLClassLoader { return classes.values(); } - synchronized void initialize(@NotNull JavaPlugin javaPlugin) { + public synchronized void initialize(@NotNull JavaPlugin javaPlugin) { // Paper Preconditions.checkArgument(javaPlugin != null, "Initializing plugin cannot be null"); Preconditions.checkArgument(javaPlugin.getClass().getClassLoader() == this, "Cannot initialize plugin outside of this class loader"); if (this.plugin != null || this.pluginInit != null) { @@ -228,6 +249,38 @@ final class PluginClassLoader extends URLClassLoader { pluginState = new IllegalStateException("Initial initialization"); this.pluginInit = javaPlugin; - javaPlugin.init(loader, loader.server, description, dataFolder, file, this); + javaPlugin.init(null, org.bukkit.Bukkit.getServer(), description, dataFolder, file, this); // Paper + } + + // Paper start + @Override + public String toString() { + JavaPlugin currPlugin = plugin != null ? plugin : pluginInit; + return "PluginClassLoader{" + + "plugin=" + currPlugin + + ", pluginEnabled=" + (currPlugin == null ? "uninitialized" : currPlugin.isEnabled()) + + ", url=" + file + + '}'; + } + + void setClass(@NotNull final String name, @NotNull final Class clazz) { + if (org.bukkit.configuration.serialization.ConfigurationSerializable.class.isAssignableFrom(clazz)) { + Class serializable = clazz.asSubclass(org.bukkit.configuration.serialization.ConfigurationSerializable.class); + org.bukkit.configuration.serialization.ConfigurationSerialization.registerClass(serializable); + } + } + + private void removeClass(@NotNull Class clazz) { + if (org.bukkit.configuration.serialization.ConfigurationSerializable.class.isAssignableFrom(clazz)) { + Class serializable = clazz.asSubclass(org.bukkit.configuration.serialization.ConfigurationSerializable.class); + org.bukkit.configuration.serialization.ConfigurationSerialization.unregisterClass(serializable); + } } + + @Override + public @Nullable io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup getGroup() { + return this.classLoaderGroup; + } + + // Paper end } diff --git a/src/test/java/io/papermc/paper/testing/TestServer.java b/src/test/java/io/papermc/paper/testing/TestServer.java index 756acf231b1b076b08046d86992ba7ce7f62a94f..52d27d977b9df0bdf02b33ed0aaa3b4db0a04cb4 100644 --- a/src/test/java/io/papermc/paper/testing/TestServer.java +++ b/src/test/java/io/papermc/paper/testing/TestServer.java @@ -36,9 +36,6 @@ public final class TestServer { when(dummyServer.getTag(anyString(), any(NamespacedKey.class), any())).thenAnswer(ignored -> new EmptyTag()); - final PluginManager pluginManager = new SimplePluginManager(dummyServer, new SimpleCommandMap(dummyServer)); - when(dummyServer.getPluginManager()).thenReturn(pluginManager); - when(dummyServer.getRegistry(any())).thenAnswer(ignored -> new EmptyRegistry()); when(dummyServer.getScoreboardCriteria(anyString())).thenReturn(null); diff --git a/src/test/java/org/bukkit/event/SyntheticEventTest.java b/src/test/java/org/bukkit/event/SyntheticEventTest.java deleted file mode 100644 index 09886568ae6167141b463b6262565fa212af3385..0000000000000000000000000000000000000000 --- a/src/test/java/org/bukkit/event/SyntheticEventTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.bukkit.event; - -import org.bukkit.plugin.PluginLoader; -import org.bukkit.plugin.SimplePluginManager; -import org.bukkit.plugin.TestPlugin; -import org.bukkit.plugin.java.JavaPluginLoader; -import org.junit.Assert; -import org.junit.Test; - -public class SyntheticEventTest { - @SuppressWarnings("deprecation") - @Test - public void test() { - io.papermc.paper.testing.TestServer.setup(); // Paper - final JavaPluginLoader loader = new JavaPluginLoader(org.bukkit.Bukkit.getServer()); // Paper - TestPlugin plugin = new TestPlugin(getClass().getName()) { - @Override - public PluginLoader getPluginLoader() { - return loader; - } - }; - SimplePluginManager pluginManager = new SimplePluginManager(org.bukkit.Bukkit.getServer(), null); // Paper - - TestEvent event = new TestEvent(false); - Impl impl = new Impl(); - - pluginManager.registerEvents(impl, plugin); - pluginManager.callEvent(event); - - Assert.assertEquals(1, impl.callCount); - } - - public abstract static class Base implements Listener { - int callCount = 0; - - public void accept(E evt) { - callCount++; - } - } - - public static class Impl extends Base { - @Override - @EventHandler - public void accept(TestEvent evt) { - super.accept(evt); - } - } -} diff --git a/src/test/java/org/bukkit/plugin/PluginManagerTest.java b/src/test/java/org/bukkit/plugin/PluginManagerTest.java deleted file mode 100644 index c46ed2acb82db814d660459b705dd49e6d44240f..0000000000000000000000000000000000000000 --- a/src/test/java/org/bukkit/plugin/PluginManagerTest.java +++ /dev/null @@ -1,183 +0,0 @@ -package org.bukkit.plugin; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import org.bukkit.event.Event; -import org.bukkit.event.TestEvent; -import org.bukkit.permissions.Permission; -import org.junit.After; -import org.junit.Test; - -public class PluginManagerTest { - private class MutableObject { - volatile Object value = null; - } - - private static final PluginManager pm = org.bukkit.Bukkit.getServer().getPluginManager(); // Paper - - private final MutableObject store = new MutableObject(); - - @Test - public void testAsyncSameThread() { - final Event event = new TestEvent(true); - try { - pm.callEvent(event); - } catch (IllegalStateException ex) { - assertThat(event.getEventName() + " cannot be triggered asynchronously from primary server thread.", is(ex.getMessage())); - return; - } - throw new IllegalStateException("No exception thrown"); - } - - @Test - public void testSyncSameThread() { - final Event event = new TestEvent(false); - pm.callEvent(event); - } - - @Test - public void testAsyncLocked() throws InterruptedException { - final Event event = new TestEvent(true); - Thread secondThread = new Thread( - new Runnable() { - @Override - public void run() { - try { - synchronized (pm) { - pm.callEvent(event); - } - } catch (Throwable ex) { - store.value = ex; - } - } - } - ); - secondThread.start(); - secondThread.join(); - assertThat(store.value, is(instanceOf(IllegalStateException.class))); - assertThat(event.getEventName() + " cannot be triggered asynchronously from inside synchronized code.", is(((Throwable) store.value).getMessage())); - } - - @Test - public void testAsyncUnlocked() throws InterruptedException { - final Event event = new TestEvent(true); - Thread secondThread = new Thread( - new Runnable() { - @Override - public void run() { - try { - pm.callEvent(event); - } catch (Throwable ex) { - store.value = ex; - } - } - } - ); - secondThread.start(); - secondThread.join(); - if (store.value != null) { - throw new RuntimeException((Throwable) store.value); - } - } - - @Test - public void testSyncUnlocked() throws InterruptedException { - final Event event = new TestEvent(false); - Thread secondThread = new Thread( - new Runnable() { - @Override - public void run() { - try { - pm.callEvent(event); - } catch (Throwable ex) { - store.value = ex; - assertThat(event.getEventName() + " cannot be triggered asynchronously from another thread.", is(ex.getMessage())); - return; - } - } - } - ); - secondThread.start(); - secondThread.join(); - if (store.value == null) { - throw new IllegalStateException("No exception thrown"); - } - } - - @Test - public void testSyncLocked() throws InterruptedException { - final Event event = new TestEvent(false); - Thread secondThread = new Thread( - new Runnable() { - @Override - public void run() { - try { - synchronized (pm) { - pm.callEvent(event); - } - } catch (Throwable ex) { - store.value = ex; - assertThat(event.getEventName() + " cannot be triggered asynchronously from another thread.", is(ex.getMessage())); - return; - } - } - } - ); - secondThread.start(); - secondThread.join(); - if (store.value == null) { - throw new IllegalStateException("No exception thrown"); - } - } - - @Test - public void testRemovePermissionByNameLower() { - this.testRemovePermissionByName("lower"); - } - - @Test - public void testRemovePermissionByNameUpper() { - this.testRemovePermissionByName("UPPER"); - } - - @Test - public void testRemovePermissionByNameCamel() { - this.testRemovePermissionByName("CaMeL"); - } - - public void testRemovePermissionByPermissionLower() { - this.testRemovePermissionByPermission("lower"); - } - - @Test - public void testRemovePermissionByPermissionUpper() { - this.testRemovePermissionByPermission("UPPER"); - } - - @Test - public void testRemovePermissionByPermissionCamel() { - this.testRemovePermissionByPermission("CaMeL"); - } - - private void testRemovePermissionByName(final String name) { - final Permission perm = new Permission(name); - pm.addPermission(perm); - assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm)); - pm.removePermission(name); - assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue())); - } - - private void testRemovePermissionByPermission(final String name) { - final Permission perm = new Permission(name); - pm.addPermission(perm); - assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm)); - pm.removePermission(perm); - assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue())); - } - - @After - public void tearDown() { - pm.clearPlugins(); - assertThat(pm.getPermissions(), is(empty())); - } -} diff --git a/src/test/java/org/bukkit/plugin/TestPlugin.java b/src/test/java/org/bukkit/plugin/TestPlugin.java index a8be3e23e3e280ad301d9530de50028515612966..43b58e920e739bb949ac0673e9ef73ba7b500dc9 100644 --- a/src/test/java/org/bukkit/plugin/TestPlugin.java +++ b/src/test/java/org/bukkit/plugin/TestPlugin.java @@ -32,6 +32,12 @@ public class TestPlugin extends PluginBase { public PluginDescriptionFile getDescription() { return new PluginDescriptionFile(pluginName, "1.0", "test.test"); } + // Paper start + @Override + public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { + return getDescription(); + } + // Paper end @Override public FileConfiguration getConfig() {