All Downloads are FREE. Search and download functionalities are using the official Maven repository.

opwvhk.plugin.Plugin Maven / Gradle / Ivy

Go to download

A simple plugin loader, loading isolated plugins with their own classpath. The application and the plugins share access to platform and service classes -- without a proxy or reflection.

The newest version!
package opwvhk.plugin;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.function.Function;

/**
 * A simple plugin: wraps a classpath and optional metadata directory, and provides access to its services and metadata.
 *
 * 

A plugin consists of a single far jar, or a directory with all classpath entries (package directories and/or jar files) and metadata (regular files). * Anything other than files and directories is ignored.

* *

Plugins can be loaded from a specific path, or a directory containing any number of plugins.

* *

Note to users: use a {@link FilteringClassLoader} or similar to avoid exposing your non-API classes.

*/ public class Plugin { private final String name; private final Path metadataPath; private final NavigableSet classpathEntries; private final ClassLoader classLoader; private final WeakHashMap, ServiceLoader> serviceLoaders; Plugin(ClassLoader parentClassLoader, Path jarPath) throws IOException { this(jarPath.getFileName().toString().substring(0, jarPath.getFileName().toString().length() - 4), null, parentClassLoader, List.of(jarPath)); } Plugin(ClassLoader parentClassLoader, Path metadataPath, List additionalClasspath) throws IOException { this(metadataPath.getFileName().toString(), metadataPath, parentClassLoader, additionalClasspath); } /** * Create a plugin. All paths in {@code additionalClasspath} are assumed to be in the {@code metadataPath}, unless {@code metadataPath} is {@code null} and * then {@code additionalClasspath} should contain exactly one entry. * * @param name the name of the plugin * @param metadataPath the metadata path, if any * @param parentClassLoader the parent classloader * @param additionalClasspath the plugin classpath in addition to the parent classpath */ private Plugin(@NotNull String name, @Nullable Path metadataPath, @NotNull ClassLoader parentClassLoader, @NotNull List additionalClasspath) throws IOException { this.name = name; this.metadataPath = metadataPath == null ? null : metadataPath.normalize().toAbsolutePath(); classpathEntries = new TreeSet<>(); URL[] classpath = new URL[additionalClasspath.size()]; int i = 0; for (Path path : additionalClasspath) { Path absolutePath = path.normalize().toAbsolutePath(); // The load method ensures the additional classpath is a single entry, or multiple entries within a plugin directory. classpathEntries.add(absolutePath); classpath[i++] = absolutePath.toUri().toURL(); } classLoader = new URLClassLoader(classpath, parentClassLoader); serviceLoaders = new WeakHashMap<>(); } public String getName() { return name; } /** * Get the {@code ServiceLoader} for a given plugin service. Uses the (isolated) plugin classloader to load classes. * * @param service the plugin service * @return the service loader */ public ServiceLoader getServiceLoader(Class service) { // noinspection unchecked return (ServiceLoader) serviceLoaders.computeIfAbsent(service, s -> ServiceLoader.load(service, classLoader)); } /** *

Load the named metadata for the plugin.

* *

Equivalent to {@code loadMetadata(metadataName, Function.identity())}

* * @param metadataName the name of the metadata file * @return the metadata if it can be loaded, {@code null} otherwise */ public byte[] loadBinaryMetadata(String metadataName) { return loadMetadata(metadataName, Function.identity()); } /** *

Load UTF-8 textual metadata for the plugin.

* *

Equivalent to {@code loadTextMetadata(metadataName, StandardCharsets.UTF_8)}

* * @param metadataName the name of the metadata * @return the metadata if it can be loaded, {@code null} otherwise * @see #loadTextMetadata(String, Charset) */ public String loadTextMetadata(String metadataName) { return loadTextMetadata(metadataName, StandardCharsets.UTF_8); } /** *

Load textual metadata for the plugin.

* *

Equivalent to {@code loadMetadata(metadataName, bytes -> new String(bytes, charset))}

* * @param metadataName the name of the metadata * @param charset the character set to use to load the metadata * @return the metadata if it can be loaded, {@code null} otherwise */ public String loadTextMetadata(String metadataName, Charset charset) { return loadMetadata(metadataName, bytes -> new String(bytes, charset)); } /** *

Load arbitrary metadata for all plugins.

* *

Any exception thrown by the mapping function is propagated.

* * @param metadataName the name of the metadata * @param mapper a function mapping the bytes read to the metadata * @return the metadata if it can be loaded, {@code null} otherwise */ public T loadMetadata(String metadataName, Function mapper) { return Optional.ofNullable(metadataPath) .map(mdp -> mdp.resolve(metadataName)) .map(Path::normalize) .map(Path::toAbsolutePath) .filter(mdp -> Optional.ofNullable(classpathEntries.floor(mdp)) .filter(mdp::startsWith) // If not empty, we're accessing the plugin classpath, which is not metadata .isEmpty()) .map(metadataFilePath -> { try { return Files.readAllBytes(metadataFilePath); } catch (IOException ignored) { return null; } }) .map(mapper) .orElse(null); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy