Paper Plugins

This commit is contained in:
Owen1212055
2022-07-06 23:00:36 -04:00
parent 844bc6c46a
commit 23095683d0
40 changed files with 1509 additions and 290 deletions

View File

@@ -0,0 +1,166 @@
package io.papermc.paper.plugin;
import java.util.List;
import java.util.Set;
import org.bukkit.permissions.Permissible;
import org.bukkit.permissions.Permission;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* A permission manager implementation to keep backwards compatibility partially alive with existing plugins that used
* the bukkit one before.
*/
@ApiStatus.Experimental
@NullMarked
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(String name);
/**
* Adds a {@link Permission} to this plugin manager.
* <p>
* 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(Permission perm);
/**
* Removes a {@link Permission} registration from this plugin manager.
* <p>
* If the specified permission does not exist in this plugin manager,
* nothing will happen.
* <p>
* Removing a permission registration will <b>not</b> remove the
* permission from any {@link Permissible}s that have it.
*
* @param perm Permission to remove
*/
void removePermission(Permission perm);
/**
* Removes a {@link Permission} registration from this plugin manager.
* <p>
* If the specified permission does not exist in this plugin manager,
* nothing will happen.
* <p>
* Removing a permission registration will <b>not</b> remove the
* permission from any {@link Permissible}s that have it.
*
* @param name Permission to remove
*/
void removePermission(String name);
/**
* Gets the default permissions for the given op status
*
* @param op Which set of default permissions to get
* @return The default permissions
*/
Set<Permission> getDefaultPermissions(boolean op);
/**
* Recalculates the defaults for the given {@link Permission}.
* <p>
* This will have no effect if the specified permission is not registered
* here.
*
* @param perm Permission to recalculate
*/
void recalculatePermissionDefaults(Permission perm);
/**
* Subscribes the given Permissible for information about the requested
* Permission, by name.
* <p>
* 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(String permission, 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(String permission, 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
*/
Set<Permissible> getPermissionSubscriptions(String permission);
/**
* Subscribes to the given Default permissions by operator status
* <p>
* 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, 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, 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
*/
Set<Permissible> getDefaultPermSubscriptions(boolean op);
/**
* Gets a set of all registered permissions.
* <p>
* This set is a copy and will not be modified live.
*
* @return Set containing all current registered permissions
*/
Set<Permission> getPermissions();
/**
* Adds a list of permissions.
* <p>
* This is meant as an optimization for adding multiple permissions without recalculating each permission.
*
* @param perm permission
*/
void addPermissions(List<Permission> perm);
/**
* Clears the current registered permissinos.
* <p>
* This is used for reloading.
*/
void clearPermissions();
}

View File

@@ -0,0 +1,16 @@
package io.papermc.paper.plugin.bootstrap;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* Represents the context provided to a {@link PluginBootstrap} during both the bootstrapping and plugin
* instantiation logic.
* A bootstrap 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
@NullMarked
@ApiStatus.NonExtendable
public interface BootstrapContext extends PluginProviderContext {
}

View File

@@ -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.jspecify.annotations.NullMarked;
/**
* A plugin bootstrap is meant for loading certain parts of the plugin before the server is loaded.
* <p>
* Plugin bootstrapping allows values to be initialized in certain parts of the server that might not be allowed
* when the server is running.
* <p>
* Your bootstrap class will be on the same classloader as your JavaPlugin.
* <p>
* <b>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</b>
*/
@ApiStatus.Experimental
@NullMarked
@ApiStatus.OverrideOnly
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(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.
*/
default JavaPlugin createPlugin(final PluginProviderContext context) {
return ProviderUtil.loadClass(context.getConfiguration().getMainClass(), JavaPlugin.class, this.getClass().getClassLoader());
}
}

View File

@@ -0,0 +1,48 @@
package io.papermc.paper.plugin.bootstrap;
import io.papermc.paper.plugin.configuration.PluginMeta;
import java.nio.file.Path;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* Represents the context provided to a {@link PluginBootstrap} during both the bootstrapping and plugin
* instantiation logic.
* A bootstrap 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
@NullMarked
@ApiStatus.NonExtendable
public interface PluginProviderContext {
/**
* Provides the plugin's configuration.
*
* @return the plugin's configuration
*/
PluginMeta getConfiguration();
/**
* Provides the path to the data directory of the plugin.
*
* @return the previously described path
*/
Path getDataDirectory();
/**
* Provides the logger used for this plugin.
*
* @return the logger instance
*/
ComponentLogger getLogger();
/**
* Provides the path to the originating source of the plugin, such as the plugin's JAR file.
*
* @return the previously described path
*/
Path getPluginSource();
}

View File

@@ -0,0 +1,186 @@
package io.papermc.paper.plugin.configuration;
import java.util.List;
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.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* This class acts as an abstraction for a plugin configuration.
*/
@ApiStatus.Experimental // Subject to change!
@NullMarked
@ApiStatus.NonExtendable
public interface PluginMeta {
/**
* Provides the name of the plugin. This name uniquely identifies the plugin amongst all loaded plugins on the
* server.
* <ul>
* <li>Will only contain alphanumeric characters, underscores, hyphens,
* and periods: [a-zA-Z0-9_\-\.].
* <li>Typically used for identifying the plugin data folder.
* <li>The name also acts as the token referenced in {@link #getPluginDependencies()},
* {@link #getPluginSoftDependencies()}, and {@link #getLoadBeforePlugins()}.
* </ul>
* <p>
* In the plugin.yml, this entry is named <code>name</code>.
* <p>
* Example:<blockquote><pre>name: MyPlugin</pre></blockquote>
*
* @return the name of the plugin
*/
String getName();
/**
* Returns the display name of the plugin, including the version.
*
* @return a descriptive name of the plugin and respective version
*/
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.
*/
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.
*/
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 semantic versioning.
*
* @return the string representation of the plugin's version
*/
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.
* <p>
* 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
*/
List<String> 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.
* <p>
* 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
*/
List<String> 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.
* <p>
* 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
*/
List<String> 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
*/
List<String> 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
*/
List<String> 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
*/
List<String> 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 <b>not guaranteed</b> 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
List<Permission> 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
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();
}

View File

@@ -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 <a href="https://docs.papermc.io/paper">Extensive documentation and examples of the paper-plugin.yml</a>
* <!--TODO update the documentation link once documentation for this exists and is deployed-->
*/
package io.papermc.paper.plugin.configuration;

View File

@@ -0,0 +1,37 @@
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.jspecify.annotations.NullMarked;
/**
* 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.Experimental
@NullMarked
@ApiStatus.NonExtendable
public interface PluginClasspathBuilder {
/**
* Adds a new classpath library to this classpath builder.
* <p>
* 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.
* <p>
* 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
*/
@Contract("_ -> this")
PluginClasspathBuilder addLibrary(ClassPathLibrary classPathLibrary);
PluginProviderContext getContext();
}

View File

@@ -0,0 +1,31 @@
package io.papermc.paper.plugin.loader;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* A plugin loader is responsible for creating certain aspects of a plugin before it is created.
* <p>
* 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.
* <p>
* 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.Experimental
@NullMarked
@ApiStatus.OverrideOnly
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(PluginClasspathBuilder classpathBuilder);
}

View File

@@ -0,0 +1,21 @@
package io.papermc.paper.plugin.loader.library;
import org.jspecify.annotations.NullMarked;
/**
* The classpath library interface represents libraries that are capable of registering themselves via
* {@link #register(LibraryStore)} on any given {@link LibraryStore}.
*/
@NullMarked
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(LibraryStore store) throws LibraryLoadingException;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,26 @@
package io.papermc.paper.plugin.loader.library;
import java.nio.file.Path;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* Represents a storage that stores library jars.
* <p>
* 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
@NullMarked
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(Path library);
}

View File

@@ -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 java.nio.file.Files;
import java.nio.file.Path;
import org.jspecify.annotations.NullMarked;
/**
* 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.
* <p>
* An example creation of the jar library type may look like this:
* <pre>{@code
* final JarLibrary customLibrary = new JarLibrary(Path.of("libs/custom-library-1.24.jar"));
* }</pre>
* resulting in a jar library that provides the jar at {@code libs/custom-library-1.24.jar} to the plugins classloader
* at runtime.
* <p>
* The jar library implementation will error if the file does not exist at the specified path.
*/
@NullMarked
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(final Path path) {
this.path = path;
}
@Override
public void register(final LibraryStore store) throws LibraryLoadingException {
if (Files.notExists(this.path)) {
throw new LibraryLoadingException("Could not find library at " + this.path);
}
store.addLibrary(this.path);
}
}

View File

@@ -0,0 +1,133 @@
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 java.io.File;
import java.util.ArrayList;
import java.util.List;
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.jspecify.annotations.NullMarked;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The maven library resolver acts as a resolver for yet to be resolved jar libraries that may be pulled from a
* remote maven repository.
* <p>
* 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:
* <pre>{@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());
* }</pre>
* <p>
* Plugins may create and register a {@link MavenLibraryResolver} after configuring it.
*/
@NullMarked
public class MavenLibraryResolver implements ClassPathLibrary {
private static final Logger LOGGER = LoggerFactory.getLogger("MavenLibraryResolver");
private final RepositorySystem repository;
private final DefaultRepositorySystemSession session;
private final List<RemoteRepository> repositories = new ArrayList<>();
private final List<Dependency> dependencies = new ArrayList<>();
/**
* Creates a new maven library resolver instance.
* <p>
* 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() {
final 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.setSystemProperties(System.getProperties());
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(final 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(final 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(final 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(final LibraryStore store) throws LibraryLoadingException {
final List<RemoteRepository> repos = this.repository.newResolutionRepositories(this.session, this.repositories);
final DependencyResult result;
try {
result = this.repository.resolveDependencies(this.session, new DependencyRequest(new CollectRequest((Dependency) null, this.dependencies, repos), null));
} catch (final DependencyResolutionException ex) {
throw new LibraryLoadingException("Error resolving libraries", ex);
}
for (final ArtifactResult artifact : result.getArtifactResults()) {
final File file = artifact.getArtifact().getFile();
store.addLibrary(file.toPath());
}
}
}

View File

@@ -0,0 +1,35 @@
package io.papermc.paper.plugin.provider.classloader;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* The class loader access interface is an <b>internal</b> representation of a class accesses' ability to see types
* from other {@link ConfiguredPluginClassLoader}.
* <p>
* 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.
*/
@NullMarked
@ApiStatus.Internal
public interface ClassLoaderAccess {
/**
* Evaluates if this class loader access is allowed to access types provided by the passed {@link
* ConfiguredPluginClassLoader}.
* <p>
* 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);
}

View File

@@ -0,0 +1,70 @@
package io.papermc.paper.plugin.provider.classloader;
import io.papermc.paper.plugin.configuration.PluginMeta;
import java.io.Closeable;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* The configured plugin class loader represents an <b>internal</b> abstraction over the classloaders used by the server
* to load and access a plugins classes during runtime.
* <p>
* It implements {@link Closeable} to define the ability to shutdown and close the classloader that implements this
* interface.
*/
@NullMarked
@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(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();
}

View File

@@ -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 <b>internal</b> type that is used to manage existing classloaders on the server.
* <p>
* 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)}.
* <p>
* Groups are differentiated into the global group or plugin owned groups.
* <ul>
* <li>The global group holds all registered class loaders and merely exists to maintain backwards compatibility with
* spigots legacy classloader handling.</li>
* <li>The plugin groups only contains the classloaders that each plugin has access to and hence serves to properly
* separates unrelated classloaders.</li>
* </ul>
*/
@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.
* <p>
* 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.
* <p>
* Note: this method is <b>highly</b> 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.
* <p>
* 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);
}

View File

@@ -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();
}

View File

@@ -0,0 +1,66 @@
package io.papermc.paper.plugin.provider.classloader;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* A plugin classloader group represents a group of classloaders that a plugins classloader may access.
* <p>
* 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.
*/
@NullMarked
@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.
* <p>
* 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.
* <p>
* This method does <b>not</b> 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();
}

View File

@@ -0,0 +1,49 @@
package io.papermc.paper.plugin.provider.entrypoint;
import io.papermc.paper.plugin.configuration.PluginMeta;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* A dependency context is a read-only abstraction of a type/concept that can resolve dependencies between plugins.
* <p>
* 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.
*/
@NullMarked
@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.
* <p>
* A simple example of this method would be
* <pre>{@code
* dependencyContext.isTransitiveDependency(pluginMetaA, pluginMetaC);
* }</pre>
* 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(PluginMeta plugin, PluginMeta depend);
/**
* Computes if this dependency context is aware of a dependency that provides/matches the passed identifier.
* <p>
* 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(String pluginIdentifier);
}

View File

@@ -0,0 +1,77 @@
package io.papermc.paper.plugin.provider.util;
import com.destroystokyo.paper.util.SneakyThrow;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* An <b>internal</b> 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.
*/
@NullMarked
@ApiStatus.Internal
public final 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 <T> 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
*/
public static <T> T loadClass(final String clazz, final Class<T> classType, final 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 <T> 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
*/
public static <T> T loadClass(final String clazz, final Class<T> classType, final ClassLoader loader, final @Nullable Runnable onError) {
try {
final T clazzInstance;
try {
final Class<?> jarClass = Class.forName(clazz, true, loader);
final Class<? extends T> pluginClass;
try {
pluginClass = jarClass.asSubclass(classType);
} catch (final ClassCastException ex) {
throw new ClassCastException("class '%s' does not extend '%s'".formatted(clazz, classType));
}
clazzInstance = pluginClass.getDeclaredConstructor().newInstance();
} catch (final IllegalAccessException exception) {
throw new RuntimeException("No public constructor");
} catch (final InstantiationException exception) {
throw new RuntimeException("Abnormal class instantiation", exception);
}
return clazzInstance;
} catch (final Throwable e) {
if (onError != null) {
onError.run();
}
SneakyThrow.sneaky(e);
}
throw new AssertionError(); // Shouldn't happen
}
}