/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pf4j;
import org.pf4j.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* This class implements the boilerplate plugin code that any {@link PluginManager}
* implementation would have to support.
* It helps cut the noise out of the subclass that handles plugin management.
*
* This class is not thread-safe.
*
* @author Decebal Suiu
*/
public abstract class AbstractPluginManager implements PluginManager {
private static final Logger log = LoggerFactory.getLogger(AbstractPluginManager.class);
public static final String PLUGINS_DIR_PROPERTY_NAME = "pf4j.pluginsDir";
public static final String MODE_PROPERTY_NAME = "pf4j.mode";
public static final String DEFAULT_PLUGINS_DIR = "plugins";
public static final String DEVELOPMENT_PLUGINS_DIR = "../plugins";
protected final List pluginsRoots = new ArrayList<>();
protected ExtensionFinder extensionFinder;
protected PluginDescriptorFinder pluginDescriptorFinder;
/**
* A map of plugins this manager is responsible for (the key is the 'pluginId').
*/
protected Map plugins;
/**
* A map of plugin class loaders (the key is the 'pluginId').
*/
protected Map pluginClassLoaders;
/**
* A list with unresolved plugins (unresolved dependency).
*/
protected List unresolvedPlugins;
/**
* A list with all resolved plugins (resolved dependency).
*/
protected List resolvedPlugins;
/**
* A list with started plugins.
*/
protected List startedPlugins;
/**
* The registered {@link PluginStateListener}s.
*/
protected List pluginStateListeners;
/**
* Cache value for the runtime mode.
* No need to re-read it because it won't change at runtime.
*/
protected RuntimeMode runtimeMode;
/**
* The system version used for comparisons to the plugin requires attribute.
*/
protected String systemVersion = "0.0.0";
protected PluginRepository pluginRepository;
protected PluginFactory pluginFactory;
protected ExtensionFactory extensionFactory;
protected PluginStatusProvider pluginStatusProvider;
protected DependencyResolver dependencyResolver;
protected PluginLoader pluginLoader;
protected boolean exactVersionAllowed = false;
protected VersionManager versionManager;
/**
* The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}.
*/
protected AbstractPluginManager() {
initialize();
}
/**
* Constructs {@code AbstractPluginManager} with the given plugins roots.
*
* @param pluginsRoots the roots to search for plugins
*/
protected AbstractPluginManager(Path... pluginsRoots) {
this(Arrays.asList(pluginsRoots));
}
/**
* Constructs {@code AbstractPluginManager} with the given plugins roots.
*
* @param pluginsRoots the roots to search for plugins
*/
protected AbstractPluginManager(List pluginsRoots) {
this.pluginsRoots.addAll(pluginsRoots);
initialize();
}
@Override
public void setSystemVersion(String version) {
systemVersion = version;
}
@Override
public String getSystemVersion() {
return systemVersion;
}
/**
* Returns a copy of plugins.
*/
@Override
public List getPlugins() {
return new ArrayList<>(plugins.values());
}
/**
* Returns a copy of plugins with that state.
*/
@Override
public List getPlugins(PluginState pluginState) {
return getPlugins().stream()
.filter(plugin -> pluginState.equals(plugin.getPluginState()))
.collect(Collectors.toList());
}
@Override
public List getResolvedPlugins() {
return resolvedPlugins;
}
@Override
public List getUnresolvedPlugins() {
return unresolvedPlugins;
}
@Override
public List getStartedPlugins() {
return startedPlugins;
}
@Override
public PluginWrapper getPlugin(String pluginId) {
return plugins.get(pluginId);
}
/**
* Load a plugin.
*
* @param pluginPath the plugin location
* @return the pluginId of the loaded plugin as specified in its {@linkplain PluginDescriptor metadata}
* @throws IllegalArgumentException if the plugin location does not exist
* @throws PluginRuntimeException if something goes wrong
*/
@Override
public String loadPlugin(Path pluginPath) {
if ((pluginPath == null) || Files.notExists(pluginPath)) {
throw new IllegalArgumentException(String.format("Specified plugin %s does not exist!", pluginPath));
}
log.debug("Loading plugin from '{}'", pluginPath);
PluginWrapper pluginWrapper = loadPluginFromPath(pluginPath);
// try to resolve the loaded plugin together with other possible plugins that depend on this plugin
resolvePlugins();
return pluginWrapper.getDescriptor().getPluginId();
}
/**
* Load plugins.
*/
@Override
public void loadPlugins() {
log.debug("Lookup plugins in '{}'", pluginsRoots);
// check for plugins roots
if (pluginsRoots.isEmpty()) {
log.warn("No plugins roots configured");
return;
}
pluginsRoots.forEach(path -> {
if (Files.notExists(path) || !Files.isDirectory(path)) {
log.warn("No '{}' root", path);
}
});
// get all plugin paths from repository
List pluginPaths = pluginRepository.getPluginPaths();
// check for no plugins
if (pluginPaths.isEmpty()) {
log.info("No plugins");
return;
}
log.debug("Found {} possible plugins: {}", pluginPaths.size(), pluginPaths);
// load plugins from plugin paths
for (Path pluginPath : pluginPaths) {
try {
loadPluginFromPath(pluginPath);
} catch (PluginRuntimeException e) {
log.error(e.getMessage(), e);
}
}
// resolve plugins
try {
resolvePlugins();
} catch (PluginRuntimeException e) {
log.error(e.getMessage(), e);
}
}
/**
* Unload all plugins
*/
@Override
public void unloadPlugins() {
// wrap resolvedPlugins in new list because of concurrent modification
for (PluginWrapper pluginWrapper : new ArrayList<>(resolvedPlugins)) {
unloadPlugin(pluginWrapper.getPluginId());
}
}
/**
* Unload the specified plugin and it's dependents.
*
* @param pluginId the pluginId of the plugin to unload
* @return true if the plugin was unloaded, otherwise false
*/
@Override
public boolean unloadPlugin(String pluginId) {
return unloadPlugin(pluginId, true);
}
/**
* Unload the specified plugin and it's dependents.
*
* @param pluginId the pluginId of the plugin to unload
* @param unloadDependents if true, unload dependents
* @return true if the plugin was unloaded, otherwise false
*/
protected boolean unloadPlugin(String pluginId, boolean unloadDependents) {
try {
if (unloadDependents) {
List dependents = dependencyResolver.getDependents(pluginId);
while (!dependents.isEmpty()) {
String dependent = dependents.remove(0);
unloadPlugin(dependent, false);
dependents.addAll(0, dependencyResolver.getDependents(dependent));
}
}
PluginWrapper pluginWrapper = getPlugin(pluginId);
PluginState pluginState;
try {
pluginState = stopPlugin(pluginId, false);
if (PluginState.STARTED == pluginState) {
return false;
}
log.info("Unload plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
} catch (Exception e) {
if (pluginWrapper == null) {
return false;
}
pluginState = PluginState.FAILED;
}
// remove the plugin
plugins.remove(pluginId);
getResolvedPlugins().remove(pluginWrapper);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
// remove the classloader
Map pluginClassLoaders = getPluginClassLoaders();
if (pluginClassLoaders.containsKey(pluginId)) {
ClassLoader classLoader = pluginClassLoaders.remove(pluginId);
if (classLoader instanceof Closeable) {
try {
((Closeable) classLoader).close();
} catch (IOException e) {
throw new PluginRuntimeException(e, "Cannot close classloader");
}
}
}
return true;
} catch (IllegalArgumentException e) {
// ignore not found exceptions because this method is recursive
}
return false;
}
@Override
public boolean deletePlugin(String pluginId) {
checkPluginId(pluginId);
PluginWrapper pluginWrapper = getPlugin(pluginId);
// stop the plugin if it's started
PluginState pluginState = stopPlugin(pluginId);
if (PluginState.STARTED == pluginState) {
log.error("Failed to stop plugin '{}' on delete", pluginId);
return false;
}
// get an instance of plugin before the plugin is unloaded
// for reason see https://github.com/pf4j/pf4j/issues/309
Plugin plugin = pluginWrapper.getPlugin();
if (!unloadPlugin(pluginId)) {
log.error("Failed to unload plugin '{}' on delete", pluginId);
return false;
}
// notify the plugin as it's deleted
plugin.delete();
return pluginRepository.deletePluginPath(pluginWrapper.getPluginPath());
}
/**
* Start all active plugins.
*/
@Override
public void startPlugins() {
for (PluginWrapper pluginWrapper : resolvedPlugins) {
PluginState pluginState = pluginWrapper.getPluginState();
if ((PluginState.DISABLED != pluginState) && (PluginState.STARTED != pluginState)) {
try {
log.info("Start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
pluginWrapper.getPlugin().start();
pluginWrapper.setPluginState(PluginState.STARTED);
pluginWrapper.setFailedException(null);
startedPlugins.add(pluginWrapper);
} catch (Exception | LinkageError e) {
pluginWrapper.setPluginState(PluginState.FAILED);
pluginWrapper.setFailedException(e);
log.error("Unable to start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()), e);
} finally {
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
}
}
}
}
/**
* Start the specified plugin and its dependencies.
*/
@Override
public PluginState startPlugin(String pluginId) {
checkPluginId(pluginId);
PluginWrapper pluginWrapper = getPlugin(pluginId);
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
PluginState pluginState = pluginWrapper.getPluginState();
if (PluginState.STARTED == pluginState) {
log.debug("Already started plugin '{}'", getPluginLabel(pluginDescriptor));
return PluginState.STARTED;
}
if (!resolvedPlugins.contains(pluginWrapper)) {
log.warn("Cannot start an unresolved plugin '{}'", getPluginLabel(pluginDescriptor));
return pluginState;
}
if (PluginState.DISABLED == pluginState) {
// automatically enable plugin on manual plugin start
if (!enablePlugin(pluginId)) {
return pluginState;
}
}
for (PluginDependency dependency : pluginDescriptor.getDependencies()) {
// start dependency only if it marked as required (non-optional) or if it optional and loaded
if (!dependency.isOptional() || plugins.containsKey(dependency.getPluginId())) {
startPlugin(dependency.getPluginId());
}
}
log.info("Start plugin '{}'", getPluginLabel(pluginDescriptor));
pluginWrapper.getPlugin().start();
pluginWrapper.setPluginState(PluginState.STARTED);
startedPlugins.add(pluginWrapper);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
return pluginWrapper.getPluginState();
}
/**
* Stop all active plugins.
*/
@Override
public void stopPlugins() {
// stop started plugins in reverse order
Collections.reverse(startedPlugins);
Iterator itr = startedPlugins.iterator();
while (itr.hasNext()) {
PluginWrapper pluginWrapper = itr.next();
PluginState pluginState = pluginWrapper.getPluginState();
if (PluginState.STARTED == pluginState) {
try {
log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
pluginWrapper.getPlugin().stop();
pluginWrapper.setPluginState(PluginState.STOPPED);
itr.remove();
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
} catch (PluginRuntimeException e) {
log.error(e.getMessage(), e);
}
} else {
// do nothing
log.debug("Plugin '{}' is not started, nothing to stop", getPluginLabel(pluginWrapper.getDescriptor()));
}
}
}
/**
* Stop the specified plugin and it's dependents.
*/
@Override
public PluginState stopPlugin(String pluginId) {
return stopPlugin(pluginId, true);
}
/**
* Stop the specified plugin and it's dependents.
*
* @param pluginId the pluginId of the plugin to stop
* @param stopDependents if true, stop dependents
* @return the plugin state after stopping
*/
protected PluginState stopPlugin(String pluginId, boolean stopDependents) {
checkPluginId(pluginId);
// test for started plugin
if (!checkPluginState(pluginId, PluginState.STARTED)) {
// do nothing
log.debug("Plugin '{}' is not started, nothing to stop", getPluginLabel(pluginId));
return getPlugin(pluginId).getPluginState();
}
if (stopDependents) {
List dependents = dependencyResolver.getDependents(pluginId);
while (!dependents.isEmpty()) {
String dependent = dependents.remove(0);
stopPlugin(dependent, false);
dependents.addAll(0, dependencyResolver.getDependents(dependent));
}
}
log.info("Stop plugin '{}'", getPluginLabel(pluginId));
PluginWrapper pluginWrapper = getPlugin(pluginId);
pluginWrapper.getPlugin().stop();
pluginWrapper.setPluginState(PluginState.STOPPED);
getStartedPlugins().remove(pluginWrapper);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, PluginState.STOPPED));
return PluginState.STOPPED;
}
/**
* Check if the plugin exists in the list of plugins.
*
* @param pluginId the pluginId to check
* @throws IllegalArgumentException if the plugin does not exist
*/
protected void checkPluginId(String pluginId) {
if (!plugins.containsKey(pluginId)) {
throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
}
}
/**
* Check if the plugin state is equals with the value passed for parameter {@code pluginState}.
*
* @param pluginId the pluginId to check
* @return {@code true} if the plugin state is equals with the value passed for parameter {@code pluginState}, otherwise {@code false}
*/
protected boolean checkPluginState(String pluginId, PluginState pluginState) {
return getPlugin(pluginId).getPluginState() == pluginState;
}
@Override
public boolean disablePlugin(String pluginId) {
checkPluginId(pluginId);
PluginWrapper pluginWrapper = getPlugin(pluginId);
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
PluginState pluginState = pluginWrapper.getPluginState();
if (PluginState.DISABLED == pluginState) {
log.debug("Already disabled plugin '{}'", getPluginLabel(pluginDescriptor));
return true;
} else if (PluginState.STARTED == pluginState) {
if (PluginState.STOPPED == stopPlugin(pluginId)) {
log.error("Failed to stop plugin '{}' on disable", getPluginLabel(pluginDescriptor));
return false;
}
}
pluginWrapper.setPluginState(PluginState.DISABLED);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
pluginStatusProvider.disablePlugin(pluginId);
log.info("Disabled plugin '{}'", getPluginLabel(pluginDescriptor));
return true;
}
@Override
public boolean enablePlugin(String pluginId) {
checkPluginId(pluginId);
PluginWrapper pluginWrapper = getPlugin(pluginId);
if (!isPluginValid(pluginWrapper)) {
log.warn("Plugin '{}' can not be enabled", getPluginLabel(pluginWrapper.getDescriptor()));
return false;
}
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
PluginState pluginState = pluginWrapper.getPluginState();
if (PluginState.DISABLED != pluginState) {
log.debug("Plugin '{}' is not disabled", getPluginLabel(pluginDescriptor));
return true;
}
pluginStatusProvider.enablePlugin(pluginId);
pluginWrapper.setPluginState(PluginState.CREATED);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
log.info("Enabled plugin '{}'", getPluginLabel(pluginDescriptor));
return true;
}
/**
* Get the {@link ClassLoader} for plugin.
*/
@Override
public ClassLoader getPluginClassLoader(String pluginId) {
return pluginClassLoaders.get(pluginId);
}
@SuppressWarnings("rawtypes")
@Override
public List> getExtensionClasses(String pluginId) {
List extensionsWrapper = extensionFinder.find(pluginId);
List> extensionClasses = new ArrayList<>(extensionsWrapper.size());
for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
Class> c = extensionWrapper.getDescriptor().extensionClass;
extensionClasses.add(c);
}
return extensionClasses;
}
@Override
public List> getExtensionClasses(Class type) {
return getExtensionClasses(extensionFinder.find(type));
}
@Override
public List> getExtensionClasses(Class type, String pluginId) {
return getExtensionClasses(extensionFinder.find(type, pluginId));
}
@Override
public List getExtensions(Class type) {
return getExtensions(extensionFinder.find(type));
}
@Override
public List getExtensions(Class type, String pluginId) {
return getExtensions(extensionFinder.find(type, pluginId));
}
@Override
@SuppressWarnings("unchecked")
public List getExtensions(String pluginId) {
List extensionsWrapper = extensionFinder.find(pluginId);
List extensions = new ArrayList<>(extensionsWrapper.size());
for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
try {
extensions.add(extensionWrapper.getExtension());
} catch (PluginRuntimeException e) {
log.error("Cannot retrieve extension", e);
}
}
return extensions;
}
@Override
public Set getExtensionClassNames(String pluginId) {
return extensionFinder.findClassNames(pluginId);
}
@Override
public ExtensionFactory getExtensionFactory() {
return extensionFactory;
}
public PluginLoader getPluginLoader() {
return pluginLoader;
}
@Override
public Path getPluginsRoot() {
return pluginsRoots.stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException("pluginsRoots have not been initialized, yet."));
}
public List getPluginsRoots() {
return Collections.unmodifiableList(pluginsRoots);
}
@Override
public RuntimeMode getRuntimeMode() {
if (runtimeMode == null) {
// retrieves the runtime mode from system
String modeAsString = System.getProperty(MODE_PROPERTY_NAME, RuntimeMode.DEPLOYMENT.toString());
runtimeMode = RuntimeMode.byName(modeAsString);
}
return runtimeMode;
}
@Override
public PluginWrapper whichPlugin(Class> clazz) {
ClassLoader classLoader = clazz.getClassLoader();
for (PluginWrapper plugin : resolvedPlugins) {
if (plugin.getPluginClassLoader() == classLoader) {
return plugin;
}
}
return null;
}
@Override
public synchronized void addPluginStateListener(PluginStateListener listener) {
pluginStateListeners.add(listener);
}
@Override
public synchronized void removePluginStateListener(PluginStateListener listener) {
pluginStateListeners.remove(listener);
}
public String getVersion() {
return Pf4jInfo.VERSION;
}
protected abstract PluginRepository createPluginRepository();
protected abstract PluginFactory createPluginFactory();
protected abstract ExtensionFactory createExtensionFactory();
protected abstract PluginDescriptorFinder createPluginDescriptorFinder();
protected abstract ExtensionFinder createExtensionFinder();
protected abstract PluginStatusProvider createPluginStatusProvider();
protected abstract PluginLoader createPluginLoader();
protected abstract VersionManager createVersionManager();
protected PluginDescriptorFinder getPluginDescriptorFinder() {
return pluginDescriptorFinder;
}
protected PluginFactory getPluginFactory() {
return pluginFactory;
}
protected Map getPluginClassLoaders() {
return pluginClassLoaders;
}
protected void initialize() {
plugins = new HashMap<>();
pluginClassLoaders = new HashMap<>();
unresolvedPlugins = new ArrayList<>();
resolvedPlugins = new ArrayList<>();
startedPlugins = new ArrayList<>();
pluginStateListeners = new ArrayList<>();
if (pluginsRoots.isEmpty()) {
pluginsRoots.addAll(createPluginsRoot());
}
pluginRepository = createPluginRepository();
pluginFactory = createPluginFactory();
extensionFactory = createExtensionFactory();
pluginDescriptorFinder = createPluginDescriptorFinder();
extensionFinder = createExtensionFinder();
pluginStatusProvider = createPluginStatusProvider();
pluginLoader = createPluginLoader();
versionManager = createVersionManager();
dependencyResolver = new DependencyResolver(versionManager);
}
/**
* Add the possibility to override the plugins roots.
* If a {@link #PLUGINS_DIR_PROPERTY_NAME} system property is defined than this method returns that roots.
* If {@link #getRuntimeMode()} returns {@link RuntimeMode#DEVELOPMENT} than {@link #DEVELOPMENT_PLUGINS_DIR}
* is returned else this method returns {@link #DEFAULT_PLUGINS_DIR}.
*
* @return the plugins root
*/
protected List createPluginsRoot() {
String pluginsDir = System.getProperty(PLUGINS_DIR_PROPERTY_NAME);
if (pluginsDir != null && !pluginsDir.isEmpty()) {
return Arrays.stream(pluginsDir.split(","))
.map(String::trim)
.map(Paths::get)
.collect(Collectors.toList());
}
pluginsDir = isDevelopment() ? DEVELOPMENT_PLUGINS_DIR : DEFAULT_PLUGINS_DIR;
return Collections.singletonList(Paths.get(pluginsDir));
}
/**
* Check if this plugin is valid (satisfies "requires" param) for a given system version.
*
* @param pluginWrapper the plugin to check
* @return true if plugin satisfies the "requires" or if requires was left blank
*/
protected boolean isPluginValid(PluginWrapper pluginWrapper) {
String requires = pluginWrapper.getDescriptor().getRequires().trim();
if (!isExactVersionAllowed() && requires.matches("^\\d+\\.\\d+\\.\\d+$")) {
// If exact versions are not allowed in requires, rewrite to >= expression
requires = ">=" + requires;
}
if (systemVersion.equals("0.0.0") || versionManager.checkVersionConstraint(systemVersion, requires)) {
return true;
}
log.warn("Plugin '{}' requires a minimum system version of {}, and you have {}",
getPluginLabel(pluginWrapper.getDescriptor()),
requires,
getSystemVersion());
return false;
}
/**
* Check if the plugin is disabled.
*
* @param pluginId the pluginId to check
* @return true if the plugin is disabled, otherwise false
*/
protected boolean isPluginDisabled(String pluginId) {
return pluginStatusProvider.isPluginDisabled(pluginId);
}
/**
* It resolves the plugins by checking the dependencies.
* It also checks for cyclic dependencies, missing dependencies and wrong versions of the dependencies.
*
* @throws PluginRuntimeException if something goes wrong
*/
protected void resolvePlugins() {
// retrieves the plugins descriptors
List descriptors = plugins.values().stream()
.map(PluginWrapper::getDescriptor)
.collect(Collectors.toList());
DependencyResolver.Result result = dependencyResolver.resolve(descriptors);
if (result.hasCyclicDependency()) {
throw new DependencyResolver.CyclicDependencyException();
}
List notFoundDependencies = result.getNotFoundDependencies();
if (!notFoundDependencies.isEmpty()) {
throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
}
List wrongVersionDependencies = result.getWrongVersionDependencies();
if (!wrongVersionDependencies.isEmpty()) {
throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
}
List sortedPlugins = result.getSortedPlugins();
// move plugins from "unresolved" to "resolved"
for (String pluginId : sortedPlugins) {
PluginWrapper pluginWrapper = plugins.get(pluginId);
if (unresolvedPlugins.remove(pluginWrapper)) {
PluginState pluginState = pluginWrapper.getPluginState();
if (pluginState != PluginState.DISABLED) {
pluginWrapper.setPluginState(PluginState.RESOLVED);
}
resolvedPlugins.add(pluginWrapper);
log.info("Plugin '{}' resolved", getPluginLabel(pluginWrapper.getDescriptor()));
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
}
}
}
/**
* Fire a plugin state event.
* This method is called when a plugin is loaded, started, stopped, etc.
*
* @param event the plugin state event
*/
protected synchronized void firePluginStateEvent(PluginStateEvent event) {
for (PluginStateListener listener : pluginStateListeners) {
log.trace("Fire '{}' to '{}'", event, listener);
listener.pluginStateChanged(event);
}
}
/**
* Load the plugin from the specified path.
*
* @param pluginPath the path to the plugin
* @return the loaded plugin
* @throws PluginAlreadyLoadedException if the plugin is already loaded
* @throws InvalidPluginDescriptorException if the plugin is invalid
*/
protected PluginWrapper loadPluginFromPath(Path pluginPath) {
// Test for plugin path duplication
String pluginId = idForPath(pluginPath);
if (pluginId != null) {
throw new PluginAlreadyLoadedException(pluginId, pluginPath);
}
// Retrieve and validate the plugin descriptor
PluginDescriptorFinder pluginDescriptorFinder = getPluginDescriptorFinder();
log.debug("Use '{}' to find plugins descriptors", pluginDescriptorFinder);
log.debug("Finding plugin descriptor for plugin '{}'", pluginPath);
PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginPath);
validatePluginDescriptor(pluginDescriptor);
// Check there are no loaded plugins with the retrieved id
pluginId = pluginDescriptor.getPluginId();
if (plugins.containsKey(pluginId)) {
PluginWrapper loadedPlugin = getPlugin(pluginId);
throw new PluginRuntimeException("There is an already loaded plugin ({}) "
+ "with the same id ({}) as the plugin at path '{}'. Simultaneous loading "
+ "of plugins with the same PluginId is not currently supported.\n"
+ "As a workaround you may include PluginVersion and PluginProvider "
+ "in PluginId.",
loadedPlugin, pluginId, pluginPath);
}
log.debug("Found descriptor {}", pluginDescriptor);
String pluginClassName = pluginDescriptor.getPluginClass();
log.debug("Class '{}' for plugin '{}'", pluginClassName, pluginPath);
// load plugin
log.debug("Loading plugin '{}'", pluginPath);
ClassLoader pluginClassLoader = getPluginLoader().loadPlugin(pluginPath, pluginDescriptor);
log.debug("Loaded plugin '{}' with class loader '{}'", pluginPath, pluginClassLoader);
PluginWrapper pluginWrapper = createPluginWrapper(pluginDescriptor, pluginPath, pluginClassLoader);
// test for disabled plugin
if (isPluginDisabled(pluginDescriptor.getPluginId())) {
log.info("Plugin '{}' is disabled", pluginPath);
pluginWrapper.setPluginState(PluginState.DISABLED);
}
// validate the plugin
if (!isPluginValid(pluginWrapper)) {
log.warn("Plugin '{}' is invalid and it will be disabled", pluginPath);
pluginWrapper.setPluginState(PluginState.DISABLED);
}
log.debug("Created wrapper '{}' for plugin '{}'", pluginWrapper, pluginPath);
pluginId = pluginDescriptor.getPluginId();
// add plugin to the list with plugins
plugins.put(pluginId, pluginWrapper);
getUnresolvedPlugins().add(pluginWrapper);
// add plugin class loader to the list with class loaders
getPluginClassLoaders().put(pluginId, pluginClassLoader);
return pluginWrapper;
}
/**
* Creates the plugin wrapper.
*
* Override this if you want to prevent plugins having full access to the plugin manager.
*
* @param pluginDescriptor the plugin descriptor
* @param pluginPath the path to the plugin
* @param pluginClassLoader the class loader for the plugin
* @return the plugin wrapper
*/
protected PluginWrapper createPluginWrapper(PluginDescriptor pluginDescriptor, Path pluginPath, ClassLoader pluginClassLoader) {
// create the plugin wrapper
log.debug("Creating wrapper for plugin '{}'", pluginPath);
PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader);
pluginWrapper.setPluginFactory(getPluginFactory());
return pluginWrapper;
}
/**
* Tests for already loaded plugins on given path.
*
* @param pluginPath the path to investigate
* @return id of plugin or null if not loaded
*/
protected String idForPath(Path pluginPath) {
for (PluginWrapper plugin : plugins.values()) {
if (plugin.getPluginPath().equals(pluginPath)) {
return plugin.getPluginId();
}
}
return null;
}
/**
* Override this to change the validation criteria.
*
* @param descriptor the plugin descriptor to validate
* @throws InvalidPluginDescriptorException if validation fails
*/
protected void validatePluginDescriptor(PluginDescriptor descriptor) {
if (StringUtils.isNullOrEmpty(descriptor.getPluginId())) {
throw new InvalidPluginDescriptorException("Field 'id' cannot be empty");
}
if (descriptor.getVersion() == null) {
throw new InvalidPluginDescriptorException("Field 'version' cannot be empty");
}
}
/**
* Check if the exact version in requires is allowed.
*
* @return true if exact versions in requires is allowed
*/
public boolean isExactVersionAllowed() {
return exactVersionAllowed;
}
/**
* Set to true to allow requires expression to be exactly x.y.z.
* The default is false, meaning that using an exact version x.y.z will
* implicitly mean the same as >=x.y.z
*
* @param exactVersionAllowed set to true or false
*/
public void setExactVersionAllowed(boolean exactVersionAllowed) {
this.exactVersionAllowed = exactVersionAllowed;
}
@Override
public VersionManager getVersionManager() {
return versionManager;
}
/**
* The plugin label is used in logging, and it's a string in format {@code pluginId@pluginVersion}.
*
* @param pluginDescriptor the plugin descriptor
* @return the plugin label
*/
protected String getPluginLabel(PluginDescriptor pluginDescriptor) {
return pluginDescriptor.getPluginId() + "@" + pluginDescriptor.getVersion();
}
/**
* Shortcut for {@code getPluginLabel(getPlugin(pluginId).getDescriptor())}.
*
* @param pluginId the pluginId
* @return the plugin label
*/
protected String getPluginLabel(String pluginId) {
return getPluginLabel(getPlugin(pluginId).getDescriptor());
}
@SuppressWarnings("unchecked")
protected List> getExtensionClasses(List> extensionsWrapper) {
List> extensionClasses = new ArrayList<>(extensionsWrapper.size());
for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
Class c = (Class) extensionWrapper.getDescriptor().extensionClass;
extensionClasses.add(c);
}
return extensionClasses;
}
protected List getExtensions(List> extensionsWrapper) {
List extensions = new ArrayList<>(extensionsWrapper.size());
for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
try {
extensions.add(extensionWrapper.getExtension());
} catch (PluginRuntimeException e) {
log.error("Cannot retrieve extension", e);
}
}
return extensions;
}
}