-/*\r
- * Copyright 2012 Decebal Suiu\r
- *\r
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with\r
- * the License. You may obtain a copy of the License in the LICENSE file, or at:\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on\r
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the\r
- * specific language governing permissions and limitations under the License.\r
- */\r
-package ro.fortsoft.pf4j;\r
-\r
-import java.io.File;\r
-import java.io.FileFilter;\r
-import java.io.IOException;\r
-import java.util.ArrayList;\r
-import java.util.Collections;\r
-import java.util.HashMap;\r
-import java.util.Iterator;\r
-import java.util.List;\r
-import java.util.Map;\r
-\r
-import org.slf4j.Logger;\r
-import org.slf4j.LoggerFactory;\r
-\r
-import ro.fortsoft.pf4j.util.AndFileFilter;\r
-import ro.fortsoft.pf4j.util.CompoundClassLoader;\r
-import ro.fortsoft.pf4j.util.DirectoryFileFilter;\r
-import ro.fortsoft.pf4j.util.FileUtils;\r
-import ro.fortsoft.pf4j.util.HiddenFilter;\r
-import ro.fortsoft.pf4j.util.NotFileFilter;\r
-import ro.fortsoft.pf4j.util.Unzip;\r
-import ro.fortsoft.pf4j.util.ZipFileFilter;\r
-\r
-/**\r
- * Default implementation of the PluginManager interface.\r
- *\r
- * @author Decebal Suiu\r
- */\r
-public class DefaultPluginManager implements PluginManager {\r
-\r
- private static final Logger log = LoggerFactory.getLogger(DefaultPluginManager.class);\r
-\r
- public static final String DEFAULT_PLUGINS_DIRECTORY = "plugins";\r
- public static final String DEVELOPMENT_PLUGINS_DIRECTORY = "../plugins";\r
-\r
- /**\r
- * The plugins repository.\r
- */\r
- private File pluginsDirectory;\r
-\r
- private ExtensionFinder extensionFinder;\r
-\r
- private PluginDescriptorFinder pluginDescriptorFinder;\r
-\r
- private PluginClasspath pluginClasspath;\r
-\r
- /**\r
- * A map of plugins this manager is responsible for (the key is the 'pluginId').\r
- */\r
- private Map<String, PluginWrapper> plugins;\r
-\r
- /**\r
- * A map of plugin class loaders (he key is the 'pluginId').\r
- */\r
- private Map<String, PluginClassLoader> pluginClassLoaders;\r
-\r
- /**\r
- * A relation between 'pluginPath' and 'pluginId'\r
- */\r
- private Map<String, String> pathToIdMap;\r
-\r
- /**\r
- * A list with unresolved plugins (unresolved dependency).\r
- */\r
- private List<PluginWrapper> unresolvedPlugins;\r
-\r
- /**\r
- * A list with resolved plugins (resolved dependency).\r
- */\r
- private List<PluginWrapper> resolvedPlugins;\r
-\r
- /**\r
- * A list with started plugins.\r
- */\r
- private List<PluginWrapper> startedPlugins;\r
-\r
- private List<String> enabledPlugins;\r
- private List<String> disabledPlugins;\r
-\r
- /**\r
- * A compound class loader of resolved plugins.\r
- */\r
- protected CompoundClassLoader compoundClassLoader;\r
-\r
- /**\r
- * Cache value for the runtime mode. No need to re-read it because it wont change at\r
- * runtime.\r
- */\r
- private RuntimeMode runtimeMode;\r
-\r
- /**\r
- * The plugins directory is supplied by System.getProperty("pf4j.pluginsDir", "plugins").\r
- */\r
- public DefaultPluginManager() {\r
- this.pluginsDirectory = createPluginsDirectory();\r
-\r
- initialize();\r
- }\r
-\r
- /**\r
- * Constructs DefaultPluginManager which the given plugins directory.\r
- *\r
- * @param pluginsDirectory\r
- * the directory to search for plugins\r
- */\r
- public DefaultPluginManager(File pluginsDirectory) {\r
- this.pluginsDirectory = pluginsDirectory;\r
-\r
- initialize();\r
- }\r
-\r
- @Override\r
- public List<PluginWrapper> getPlugins() {\r
- return new ArrayList<PluginWrapper>(plugins.values());\r
- }\r
-\r
- @Override\r
- public List<PluginWrapper> getResolvedPlugins() {\r
- return resolvedPlugins;\r
- }\r
-\r
- public PluginWrapper getPlugin(String pluginId) {\r
- return plugins.get(pluginId);\r
- }\r
-\r
- @Override\r
- public List<PluginWrapper> getUnresolvedPlugins() {\r
- return unresolvedPlugins;\r
- }\r
-\r
- @Override\r
- public List<PluginWrapper> getStartedPlugins() {\r
- return startedPlugins;\r
- }\r
-\r
- /**\r
- * Start all active plugins.\r
- */\r
- @Override\r
- public void startPlugins() {\r
- for (PluginWrapper pluginWrapper : resolvedPlugins) {\r
- try {\r
- log.info("Start plugin '{}'", pluginWrapper.getDescriptor().getPluginId());\r
- pluginWrapper.getPlugin().start();\r
- pluginWrapper.setPluginState(PluginState.STARTED);\r
- startedPlugins.add(pluginWrapper);\r
- } catch (PluginException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- }\r
- }\r
-\r
- /**\r
- * Start the specified plugin and it's dependencies.\r
- */\r
- @Override\r
- public PluginState startPlugin(String pluginId) {\r
- if (!plugins.containsKey(pluginId)) {\r
- throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));\r
- }\r
- PluginWrapper pluginWrapper = plugins.get(pluginId);\r
- PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();\r
- if (pluginWrapper.getPluginState().equals(PluginState.STARTED)) {\r
- log.debug("Already started plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());\r
- return PluginState.STARTED;\r
- }\r
- for (PluginDependency dependency : pluginDescriptor.getDependencies()) {\r
- startPlugin(dependency.getPluginId());\r
- }\r
- try {\r
- log.info("Start plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());\r
- pluginWrapper.getPlugin().start();\r
- pluginWrapper.setPluginState(PluginState.STARTED);\r
- startedPlugins.add(pluginWrapper);\r
- } catch (PluginException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- return pluginWrapper.getPluginState();\r
- }\r
-\r
- /**\r
- * Stop all active plugins.\r
- */\r
- @Override\r
- public void stopPlugins() {\r
- // stop started plugins in reverse order\r
- Collections.reverse(startedPlugins);\r
- Iterator<PluginWrapper> itr = startedPlugins.iterator();\r
- while (itr.hasNext()) {\r
- PluginWrapper pluginWrapper = itr.next();\r
- PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();\r
- try {\r
- log.info("Stop plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());\r
- pluginWrapper.getPlugin().stop();\r
- pluginWrapper.setPluginState(PluginState.STOPPED);\r
- itr.remove();\r
- } catch (PluginException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- }\r
- }\r
-\r
- /**\r
- * Stop the specified plugin and it's dependencies.\r
- */\r
- @Override\r
- public PluginState stopPlugin(String pluginId) {\r
- if (!plugins.containsKey(pluginId)) {\r
- throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));\r
- }\r
- PluginWrapper pluginWrapper = plugins.get(pluginId);\r
- PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();\r
- if (pluginWrapper.getPluginState().equals(PluginState.STOPPED)) {\r
- log.debug("Already stopped plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());\r
- return PluginState.STOPPED;\r
- }\r
- for (PluginDependency dependency : pluginDescriptor.getDependencies()) {\r
- stopPlugin(dependency.getPluginId());\r
- }\r
- try {\r
- log.info("Stop plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());\r
- pluginWrapper.getPlugin().stop();\r
- pluginWrapper.setPluginState(PluginState.STOPPED);\r
- startedPlugins.remove(pluginWrapper);\r
- } catch (PluginException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- return pluginWrapper.getPluginState();\r
- }\r
-\r
- /**\r
- * Load plugins.\r
- */\r
- @Override\r
- public void loadPlugins() {\r
- log.debug("Lookup plugins in '{}'", pluginsDirectory.getAbsolutePath());\r
- // check for plugins directory\r
- if (!pluginsDirectory.exists() || !pluginsDirectory.isDirectory()) {\r
- log.error("No '{}' directory", pluginsDirectory.getAbsolutePath());\r
- return;\r
- }\r
-\r
- // expand all plugin archives\r
- FileFilter zipFilter = new ZipFileFilter();\r
- File[] zipFiles = pluginsDirectory.listFiles(zipFilter);\r
- if (zipFiles != null) {\r
- for (File zipFile : zipFiles) {\r
- try {\r
- expandPluginArchive(zipFile);\r
- } catch (IOException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- }\r
- }\r
-\r
- // check for no plugins\r
- List<FileFilter> filterList = new ArrayList<FileFilter>();\r
- filterList.add(new DirectoryFileFilter());\r
- filterList.add(new NotFileFilter(createHiddenPluginFilter()));\r
- FileFilter pluginsFilter = new AndFileFilter(filterList);\r
- File[] directories = pluginsDirectory.listFiles(pluginsFilter);\r
- if (directories == null) {\r
- directories = new File[0];\r
- }\r
- log.debug("Found possible {} plugins: {}", directories.length, directories);\r
- if (directories.length == 0) {\r
- log.info("No plugins");\r
- return;\r
- }\r
-\r
- // load any plugin from plugins directory\r
- for (File directory : directories) {\r
- try {\r
- loadPlugin(directory);\r
- } catch (PluginException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- }\r
-\r
- // resolve 'unresolvedPlugins'\r
- try {\r
- resolvePlugins();\r
- } catch (PluginException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- }\r
-\r
- @Override\r
- public boolean unloadPlugin(String pluginId) {\r
- try {\r
- PluginState state = stopPlugin(pluginId);\r
- if (!PluginState.STOPPED.equals(state)) {\r
- return false;\r
- }\r
-\r
- PluginWrapper pluginWrapper = plugins.get(pluginId);\r
- PluginDescriptor descriptor = pluginWrapper.getDescriptor();\r
- List<PluginDependency> dependencies = descriptor.getDependencies();\r
- for (PluginDependency dependency : dependencies) {\r
- if (!unloadPlugin(dependency.getPluginId())) {\r
- return false;\r
- }\r
- }\r
-\r
- // remove the plugin\r
- plugins.remove(pluginId);\r
- resolvedPlugins.remove(pluginWrapper);\r
- pathToIdMap.remove(pluginWrapper.getPluginPath());\r
-\r
- // remove the classloader\r
- if (pluginClassLoaders.containsKey(pluginId)) {\r
- PluginClassLoader classLoader = pluginClassLoaders.remove(pluginId);\r
- compoundClassLoader.removeLoader(classLoader);\r
- try {\r
- classLoader.close();\r
- } catch (IOException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
- }\r
- return true;\r
- } catch (IllegalArgumentException e) {\r
- // ignore not found exceptions because this method is recursive\r
- }\r
- return false;\r
- }\r
-\r
- @Override\r
- public boolean deletePlugin(String pluginId) {\r
- if (!plugins.containsKey(pluginId)) {\r
- throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));\r
- }\r
- PluginWrapper pw = getPlugin(pluginId);\r
- PluginState state = stopPlugin(pluginId);\r
-\r
- if (PluginState.STOPPED != state) {\r
- log.error("Failed to stop plugin {} on delete", pluginId);\r
- return false;\r
- }\r
-\r
- if (!unloadPlugin(pluginId)) {\r
- log.error("Failed to unload plugin {} on delete", pluginId);\r
- return false;\r
- }\r
-\r
- File pluginFolder = new File(pluginsDirectory, pw.getPluginPath());\r
- File pluginZip = null;\r
-\r
- FileFilter zipFilter = new ZipFileFilter();\r
- File[] zipFiles = pluginsDirectory.listFiles(zipFilter);\r
- if (zipFiles != null) {\r
- // strip prepended / from the plugin path\r
- String dirName = pw.getPluginPath().substring(1);\r
- // find the zip file that matches the plugin path\r
- for (File zipFile : zipFiles) {\r
- String name = zipFile.getName().substring(0, zipFile.getName().lastIndexOf('.'));\r
- if (name.equals(dirName)) {\r
- pluginZip = zipFile;\r
- break;\r
- }\r
- }\r
- }\r
-\r
- if (pluginFolder.exists()) {\r
- FileUtils.delete(pluginFolder);\r
- }\r
- if (pluginZip != null && pluginZip.exists()) {\r
- FileUtils.delete(pluginZip);\r
- }\r
- return true;\r
- }\r
-\r
- /**\r
- * Get plugin class loader for this path.\r
- */\r
- @Override\r
- public PluginClassLoader getPluginClassLoader(String pluginId) {\r
- return pluginClassLoaders.get(pluginId);\r
- }\r
-\r
- @Override\r
- public <T> List<T> getExtensions(Class<T> type) {\r
- List<ExtensionWrapper<T>> extensionsWrapper = extensionFinder.find(type);\r
- List<T> extensions = new ArrayList<T>(extensionsWrapper.size());\r
- for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {\r
- extensions.add(extensionWrapper.getInstance());\r
- }\r
-\r
- return extensions;\r
- }\r
-\r
- @Override\r
- public RuntimeMode getRuntimeMode() {\r
- if (runtimeMode == null) {\r
- // retrieves the runtime mode from system\r
- String modeAsString = System.getProperty("pf4j.mode", RuntimeMode.DEPLOYMENT.toString());\r
- runtimeMode = RuntimeMode.byName(modeAsString);\r
-\r
- log.info("PF4J runtime mode is '{}'", runtimeMode);\r
-\r
- }\r
-\r
- return runtimeMode;\r
- }\r
-\r
- /**\r
- * Retrieves the {@link PluginWrapper} that loaded the given class 'clazz'.\r
- */\r
- public PluginWrapper whichPlugin(Class<?> clazz) {\r
- ClassLoader classLoader = clazz.getClassLoader();\r
- for (PluginWrapper plugin : resolvedPlugins) {\r
- if (plugin.getPluginClassLoader() == classLoader) {\r
- return plugin;\r
- }\r
- }\r
-\r
- return null;\r
- }\r
-\r
- /**\r
- * Add the possibility to override the PluginDescriptorFinder.\r
- * By default if getRuntimeMode() returns RuntimeMode.DEVELOPMENT than a\r
- * PropertiesPluginDescriptorFinder is returned else this method returns\r
- * DefaultPluginDescriptorFinder.\r
- */\r
- protected PluginDescriptorFinder createPluginDescriptorFinder() {\r
- if (RuntimeMode.DEVELOPMENT.equals(getRuntimeMode())) {\r
- return new PropertiesPluginDescriptorFinder();\r
- }\r
-\r
- return new DefaultPluginDescriptorFinder(pluginClasspath);\r
- }\r
-\r
- /**\r
- * Add the possibility to override the ExtensionFinder.\r
- */\r
- protected ExtensionFinder createExtensionFinder() {\r
- return new DefaultExtensionFinder(compoundClassLoader);\r
- }\r
-\r
- /**\r
- * Add the possibility to override the PluginClassPath.\r
- * By default if getRuntimeMode() returns RuntimeMode.DEVELOPMENT than a\r
- * DevelopmentPluginClasspath is returned else this method returns\r
- * PluginClasspath.\r
- */\r
- protected PluginClasspath createPluginClasspath() {\r
- if (RuntimeMode.DEVELOPMENT.equals(getRuntimeMode())) {\r
- return new DevelopmentPluginClasspath();\r
- }\r
-\r
- return new PluginClasspath();\r
- }\r
-\r
- protected boolean isPluginDisabled(String pluginId) {\r
- if (enabledPlugins.isEmpty()) {\r
- return disabledPlugins.contains(pluginId);\r
- }\r
-\r
- return !enabledPlugins.contains(pluginId);\r
- }\r
-\r
- protected FileFilter createHiddenPluginFilter() {\r
- return new HiddenFilter();\r
- }\r
-\r
- /**\r
- * Add the possibility to override the plugins directory.\r
- * If a "pf4j.pluginsDir" system property is defined than this method returns\r
- * that directory.\r
- * If getRuntimeMode() returns RuntimeMode.DEVELOPMENT than a\r
- * DEVELOPMENT_PLUGINS_DIRECTORY ("../plugins") is returned else this method returns\r
- * DEFAULT_PLUGINS_DIRECTORY ("plugins").\r
- * @return\r
- */\r
- protected File createPluginsDirectory() {\r
- String pluginsDir = System.getProperty("pf4j.pluginsDir");\r
- if (pluginsDir == null) {\r
- if (RuntimeMode.DEVELOPMENT.equals(getRuntimeMode())) {\r
- pluginsDir = DEVELOPMENT_PLUGINS_DIRECTORY;\r
- } else {\r
- pluginsDir = DEFAULT_PLUGINS_DIRECTORY;\r
- }\r
- }\r
-\r
- return new File(pluginsDir);\r
- }\r
-\r
- private void initialize() {\r
- plugins = new HashMap<String, PluginWrapper>();\r
- pluginClassLoaders = new HashMap<String, PluginClassLoader>();\r
- pathToIdMap = new HashMap<String, String>();\r
- unresolvedPlugins = new ArrayList<PluginWrapper>();\r
- resolvedPlugins = new ArrayList<PluginWrapper>();\r
- startedPlugins = new ArrayList<PluginWrapper>();\r
- disabledPlugins = new ArrayList<String>();\r
- compoundClassLoader = new CompoundClassLoader();\r
-\r
- pluginClasspath = createPluginClasspath();\r
- pluginDescriptorFinder = createPluginDescriptorFinder();\r
- extensionFinder = createExtensionFinder();\r
-\r
- try {\r
- // create a list with plugin identifiers that should be only accepted by this manager (whitelist from plugins/enabled.txt file)\r
- enabledPlugins = FileUtils.readLines(new File(pluginsDirectory, "enabled.txt"), true);\r
- log.info("Enabled plugins: {}", enabledPlugins);\r
-\r
- // create a list with plugin identifiers that should not be accepted by this manager (blacklist from plugins/disabled.txt file)\r
- disabledPlugins = FileUtils.readLines(new File(pluginsDirectory, "disabled.txt"), true);\r
- log.info("Disabled plugins: {}", disabledPlugins);\r
- } catch (IOException e) {\r
- log.error(e.getMessage(), e);\r
- }\r
-\r
- System.setProperty("pf4j.pluginsDir", pluginsDirectory.getAbsolutePath());\r
- }\r
-\r
- private void loadPlugin(File pluginDirectory) throws PluginException {\r
- // try to load the plugin\r
- String pluginName = pluginDirectory.getName();\r
- String pluginPath = "/".concat(pluginName);\r
-\r
- // test for plugin duplication\r
- if (plugins.get(pathToIdMap.get(pluginPath)) != null) {\r
- return;\r
- }\r
-\r
- // retrieves the plugin descriptor\r
- log.debug("Find plugin descriptor '{}'", pluginPath);\r
- PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginDirectory);\r
- log.debug("Descriptor " + pluginDescriptor);\r
- String pluginClassName = pluginDescriptor.getPluginClass();\r
- log.debug("Class '{}' for plugin '{}'", pluginClassName, pluginPath);\r
-\r
- // test for disabled plugin\r
- if (isPluginDisabled(pluginDescriptor.getPluginId())) {\r
- log.info("Plugin '{}' is disabled", pluginPath);\r
- return;\r
- }\r
-\r
- // load plugin\r
- log.debug("Loading plugin '{}'", pluginPath);\r
- PluginLoader pluginLoader = new PluginLoader(this, pluginDescriptor, pluginDirectory, pluginClasspath);\r
- pluginLoader.load();\r
- log.debug("Loaded plugin '{}'", pluginPath);\r
-\r
- // create the plugin wrapper\r
- log.debug("Creating wrapper for plugin '{}'", pluginPath);\r
- PluginWrapper pluginWrapper = new PluginWrapper(pluginDescriptor, pluginPath, pluginLoader.getPluginClassLoader());\r
- pluginWrapper.setRuntimeMode(getRuntimeMode());\r
- log.debug("Created wrapper '{}' for plugin '{}'", pluginWrapper, pluginPath);\r
-\r
- String pluginId = pluginDescriptor.getPluginId();\r
-\r
- // add plugin to the list with plugins\r
- plugins.put(pluginId, pluginWrapper);\r
- unresolvedPlugins.add(pluginWrapper);\r
-\r
- // add plugin class loader to the list with class loaders\r
- PluginClassLoader pluginClassLoader = pluginLoader.getPluginClassLoader();\r
- pluginClassLoaders.put(pluginId, pluginClassLoader);\r
- }\r
-\r
- private void expandPluginArchive(File pluginArchiveFile) throws IOException {\r
- String fileName = pluginArchiveFile.getName();\r
- long pluginArchiveDate = pluginArchiveFile.lastModified();\r
- String pluginName = fileName.substring(0, fileName.length() - 4);\r
- File pluginDirectory = new File(pluginsDirectory, pluginName);\r
- // check if exists directory or the '.zip' file is "newer" than directory\r
- if (!pluginDirectory.exists() || (pluginArchiveDate > pluginDirectory.lastModified())) {\r
- log.debug("Expand plugin archive '{}' in '{}'", pluginArchiveFile, pluginDirectory);\r
- // create directory for plugin\r
- pluginDirectory.mkdirs();\r
-\r
- // expand '.zip' file\r
- Unzip unzip = new Unzip();\r
- unzip.setSource(pluginArchiveFile);\r
- unzip.setDestination(pluginDirectory);\r
- unzip.extract();\r
- }\r
- }\r
-\r
- private void resolvePlugins() throws PluginException {\r
- resolveDependencies();\r
- }\r
-\r
- private void resolveDependencies() throws PluginException {\r
- DependencyResolver dependencyResolver = new DependencyResolver(unresolvedPlugins);\r
- resolvedPlugins = dependencyResolver.getSortedPlugins();\r
- for (PluginWrapper pluginWrapper : resolvedPlugins) {\r
- unresolvedPlugins.remove(pluginWrapper);\r
- compoundClassLoader.addLoader(pluginWrapper.getPluginClassLoader());\r
- log.info("Plugin '{}' resolved", pluginWrapper.getDescriptor().getPluginId());\r
- }\r
- }\r
-\r
-}\r
+/*
+ * Copyright 2012 Decebal Suiu
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with
+ * the License. You may obtain a copy of the License in the LICENSE file, or 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 ro.fortsoft.pf4j;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ro.fortsoft.pf4j.util.AndFileFilter;
+import ro.fortsoft.pf4j.util.CompoundClassLoader;
+import ro.fortsoft.pf4j.util.DirectoryFileFilter;
+import ro.fortsoft.pf4j.util.FileUtils;
+import ro.fortsoft.pf4j.util.HiddenFilter;
+import ro.fortsoft.pf4j.util.NotFileFilter;
+import ro.fortsoft.pf4j.util.Unzip;
+import ro.fortsoft.pf4j.util.ZipFileFilter;
+
+/**
+ * Default implementation of the PluginManager interface.
+ *
+ * @author Decebal Suiu
+ */
+public class DefaultPluginManager implements PluginManager {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultPluginManager.class);
+
+ public static final String DEFAULT_PLUGINS_DIRECTORY = "plugins";
+ public static final String DEVELOPMENT_PLUGINS_DIRECTORY = "../plugins";
+
+ /**
+ * The plugins repository.
+ */
+ private File pluginsDirectory;
+
+ private ExtensionFinder extensionFinder;
+
+ private PluginDescriptorFinder pluginDescriptorFinder;
+
+ private PluginClasspath pluginClasspath;
+
+ /**
+ * A map of plugins this manager is responsible for (the key is the 'pluginId').
+ */
+ private Map<String, PluginWrapper> plugins;
+
+ /**
+ * A map of plugin class loaders (he key is the 'pluginId').
+ */
+ private Map<String, PluginClassLoader> pluginClassLoaders;
+
+ /**
+ * A relation between 'pluginPath' and 'pluginId'
+ */
+ private Map<String, String> pathToIdMap;
+
+ /**
+ * A list with unresolved plugins (unresolved dependency).
+ */
+ private List<PluginWrapper> unresolvedPlugins;
+
+ /**
+ * A list with resolved plugins (resolved dependency).
+ */
+ private List<PluginWrapper> resolvedPlugins;
+
+ /**
+ * A list with started plugins.
+ */
+ private List<PluginWrapper> startedPlugins;
+
+ private List<String> enabledPlugins;
+ private List<String> disabledPlugins;
+
+ /**
+ * A compound class loader of resolved plugins.
+ */
+ protected CompoundClassLoader compoundClassLoader;
+
+ /**
+ * Cache value for the runtime mode. No need to re-read it because it wont change at
+ * runtime.
+ */
+ private RuntimeMode runtimeMode;
+
+ /**
+ * The plugins directory is supplied by System.getProperty("pf4j.pluginsDir", "plugins").
+ */
+ public DefaultPluginManager() {
+ this.pluginsDirectory = createPluginsDirectory();
+
+ initialize();
+ }
+
+ /**
+ * Constructs DefaultPluginManager which the given plugins directory.
+ *
+ * @param pluginsDirectory
+ * the directory to search for plugins
+ */
+ public DefaultPluginManager(File pluginsDirectory) {
+ this.pluginsDirectory = pluginsDirectory;
+
+ initialize();
+ }
+
+ @Override
+ public List<PluginWrapper> getPlugins() {
+ return new ArrayList<PluginWrapper>(plugins.values());
+ }
+
+ @Override
+ public List<PluginWrapper> getResolvedPlugins() {
+ return resolvedPlugins;
+ }
+
+ public PluginWrapper getPlugin(String pluginId) {
+ return plugins.get(pluginId);
+ }
+
+ @Override
+ public List<PluginWrapper> getUnresolvedPlugins() {
+ return unresolvedPlugins;
+ }
+
+ @Override
+ public List<PluginWrapper> getStartedPlugins() {
+ return startedPlugins;
+ }
+
+ @Override
+ public String loadPlugin(File pluginArchiveFile) {
+ if (pluginArchiveFile == null || !pluginArchiveFile.exists()) {
+ throw new IllegalArgumentException(String.format("Specified plugin %s does not exist!", pluginArchiveFile));
+ }
+
+ File pluginDirectory = null;
+ try {
+ pluginDirectory = expandPluginArchive(pluginArchiveFile);
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ if (pluginDirectory == null || !pluginDirectory.exists()) {
+ throw new IllegalArgumentException(String.format("Failed to expand %s", pluginArchiveFile));
+ }
+
+ try {
+ PluginWrapper pluginWrapper = loadPluginDirectory(pluginDirectory);
+ // TODO uninstalled plugin dependencies?
+ unresolvedPlugins.remove(pluginWrapper);
+ resolvedPlugins.add(pluginWrapper);
+ compoundClassLoader.addLoader(pluginWrapper.getPluginClassLoader());
+ extensionFinder.reset();
+ return pluginWrapper.getDescriptor().getPluginId();
+ } catch (PluginException e) {
+ log.error(e.getMessage(), e);
+ }
+ return null;
+ }
+
+ /**
+ * Start all active plugins.
+ */
+ @Override
+ public void startPlugins() {
+ for (PluginWrapper pluginWrapper : resolvedPlugins) {
+ try {
+ log.info("Start plugin '{}'", pluginWrapper.getDescriptor().getPluginId());
+ pluginWrapper.getPlugin().start();
+ pluginWrapper.setPluginState(PluginState.STARTED);
+ startedPlugins.add(pluginWrapper);
+ } catch (PluginException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Start the specified plugin and it's dependencies.
+ */
+ @Override
+ public PluginState startPlugin(String pluginId) {
+ if (!plugins.containsKey(pluginId)) {
+ throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
+ }
+ PluginWrapper pluginWrapper = plugins.get(pluginId);
+ PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
+ if (pluginWrapper.getPluginState().equals(PluginState.STARTED)) {
+ log.debug("Already started plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());
+ return PluginState.STARTED;
+ }
+ for (PluginDependency dependency : pluginDescriptor.getDependencies()) {
+ startPlugin(dependency.getPluginId());
+ }
+ try {
+ log.info("Start plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());
+ pluginWrapper.getPlugin().start();
+ pluginWrapper.setPluginState(PluginState.STARTED);
+ startedPlugins.add(pluginWrapper);
+ } catch (PluginException e) {
+ log.error(e.getMessage(), e);
+ }
+ return pluginWrapper.getPluginState();
+ }
+
+ /**
+ * Stop all active plugins.
+ */
+ @Override
+ public void stopPlugins() {
+ // stop started plugins in reverse order
+ Collections.reverse(startedPlugins);
+ Iterator<PluginWrapper> itr = startedPlugins.iterator();
+ while (itr.hasNext()) {
+ PluginWrapper pluginWrapper = itr.next();
+ PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
+ try {
+ log.info("Stop plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());
+ pluginWrapper.getPlugin().stop();
+ pluginWrapper.setPluginState(PluginState.STOPPED);
+ itr.remove();
+ } catch (PluginException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Stop the specified plugin and it's dependencies.
+ */
+ @Override
+ public PluginState stopPlugin(String pluginId) {
+ if (!plugins.containsKey(pluginId)) {
+ throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
+ }
+ PluginWrapper pluginWrapper = plugins.get(pluginId);
+ PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
+ if (pluginWrapper.getPluginState().equals(PluginState.STOPPED)) {
+ log.debug("Already stopped plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());
+ return PluginState.STOPPED;
+ }
+ for (PluginDependency dependency : pluginDescriptor.getDependencies()) {
+ stopPlugin(dependency.getPluginId());
+ }
+ try {
+ log.info("Stop plugin '{}:{}'", pluginDescriptor.getPluginId(), pluginDescriptor.getVersion());
+ pluginWrapper.getPlugin().stop();
+ pluginWrapper.setPluginState(PluginState.STOPPED);
+ startedPlugins.remove(pluginWrapper);
+ } catch (PluginException e) {
+ log.error(e.getMessage(), e);
+ }
+ return pluginWrapper.getPluginState();
+ }
+
+ /**
+ * Load plugins.
+ */
+ @Override
+ public void loadPlugins() {
+ log.debug("Lookup plugins in '{}'", pluginsDirectory.getAbsolutePath());
+ // check for plugins directory
+ if (!pluginsDirectory.exists() || !pluginsDirectory.isDirectory()) {
+ log.error("No '{}' directory", pluginsDirectory.getAbsolutePath());
+ return;
+ }
+
+ // expand all plugin archives
+ FileFilter zipFilter = new ZipFileFilter();
+ File[] zipFiles = pluginsDirectory.listFiles(zipFilter);
+ if (zipFiles != null) {
+ for (File zipFile : zipFiles) {
+ try {
+ expandPluginArchive(zipFile);
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ }
+
+ // check for no plugins
+ List<FileFilter> filterList = new ArrayList<FileFilter>();
+ filterList.add(new DirectoryFileFilter());
+ filterList.add(new NotFileFilter(createHiddenPluginFilter()));
+ FileFilter pluginsFilter = new AndFileFilter(filterList);
+ File[] directories = pluginsDirectory.listFiles(pluginsFilter);
+ if (directories == null) {
+ directories = new File[0];
+ }
+ log.debug("Found possible {} plugins: {}", directories.length, directories);
+ if (directories.length == 0) {
+ log.info("No plugins");
+ return;
+ }
+
+ // load any plugin from plugins directory
+ for (File directory : directories) {
+ try {
+ loadPluginDirectory(directory);
+ } catch (PluginException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ // resolve 'unresolvedPlugins'
+ try {
+ resolvePlugins();
+ } catch (PluginException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean unloadPlugin(String pluginId) {
+ try {
+ PluginState state = stopPlugin(pluginId);
+ if (!PluginState.STOPPED.equals(state)) {
+ return false;
+ }
+
+ PluginWrapper pluginWrapper = plugins.get(pluginId);
+ PluginDescriptor descriptor = pluginWrapper.getDescriptor();
+ List<PluginDependency> dependencies = descriptor.getDependencies();
+ for (PluginDependency dependency : dependencies) {
+ if (!unloadPlugin(dependency.getPluginId())) {
+ return false;
+ }
+ }
+
+ // remove the plugin
+ plugins.remove(pluginId);
+ resolvedPlugins.remove(pluginWrapper);
+ pathToIdMap.remove(pluginWrapper.getPluginPath());
+ extensionFinder.reset();
+
+ // remove the classloader
+ if (pluginClassLoaders.containsKey(pluginId)) {
+ PluginClassLoader classLoader = pluginClassLoaders.remove(pluginId);
+ compoundClassLoader.removeLoader(classLoader);
+ try {
+ classLoader.close();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return true;
+ } catch (IllegalArgumentException e) {
+ // ignore not found exceptions because this method is recursive
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deletePlugin(String pluginId) {
+ if (!plugins.containsKey(pluginId)) {
+ throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
+ }
+ PluginWrapper pw = getPlugin(pluginId);
+ PluginState state = stopPlugin(pluginId);
+
+ if (PluginState.STOPPED != state) {
+ log.error("Failed to stop plugin {} on delete", pluginId);
+ return false;
+ }
+
+ if (!unloadPlugin(pluginId)) {
+ log.error("Failed to unload plugin {} on delete", pluginId);
+ return false;
+ }
+
+ File pluginFolder = new File(pluginsDirectory, pw.getPluginPath());
+ File pluginZip = null;
+
+ FileFilter zipFilter = new ZipFileFilter();
+ File[] zipFiles = pluginsDirectory.listFiles(zipFilter);
+ if (zipFiles != null) {
+ // strip prepended / from the plugin path
+ String dirName = pw.getPluginPath().substring(1);
+ // find the zip file that matches the plugin path
+ for (File zipFile : zipFiles) {
+ String name = zipFile.getName().substring(0, zipFile.getName().lastIndexOf('.'));
+ if (name.equals(dirName)) {
+ pluginZip = zipFile;
+ break;
+ }
+ }
+ }
+
+ if (pluginFolder.exists()) {
+ FileUtils.delete(pluginFolder);
+ }
+ if (pluginZip != null && pluginZip.exists()) {
+ FileUtils.delete(pluginZip);
+ }
+ return true;
+ }
+
+ /**
+ * Get plugin class loader for this path.
+ */
+ @Override
+ public PluginClassLoader getPluginClassLoader(String pluginId) {
+ return pluginClassLoaders.get(pluginId);
+ }
+
+ @Override
+ public <T> List<T> getExtensions(Class<T> type) {
+ List<ExtensionWrapper<T>> extensionsWrapper = extensionFinder.find(type);
+ List<T> extensions = new ArrayList<T>(extensionsWrapper.size());
+ for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {
+ extensions.add(extensionWrapper.getInstance());
+ }
+
+ return extensions;
+ }
+
+ @Override
+ public RuntimeMode getRuntimeMode() {
+ if (runtimeMode == null) {
+ // retrieves the runtime mode from system
+ String modeAsString = System.getProperty("pf4j.mode", RuntimeMode.DEPLOYMENT.toString());
+ runtimeMode = RuntimeMode.byName(modeAsString);
+
+ log.info("PF4J runtime mode is '{}'", runtimeMode);
+
+ }
+
+ return runtimeMode;
+ }
+
+ /**
+ * Retrieves the {@link PluginWrapper} that loaded the given class 'clazz'.
+ */
+ public PluginWrapper whichPlugin(Class<?> clazz) {
+ ClassLoader classLoader = clazz.getClassLoader();
+ for (PluginWrapper plugin : resolvedPlugins) {
+ if (plugin.getPluginClassLoader() == classLoader) {
+ return plugin;
+ }
+ }
+ log.warn("Failed to find the plugin for {}", clazz);
+ return null;
+ }
+
+ /**
+ * Add the possibility to override the PluginDescriptorFinder.
+ * By default if getRuntimeMode() returns RuntimeMode.DEVELOPMENT than a
+ * PropertiesPluginDescriptorFinder is returned else this method returns
+ * DefaultPluginDescriptorFinder.
+ */
+ protected PluginDescriptorFinder createPluginDescriptorFinder() {
+ if (RuntimeMode.DEVELOPMENT.equals(getRuntimeMode())) {
+ return new PropertiesPluginDescriptorFinder();
+ }
+
+ return new DefaultPluginDescriptorFinder(pluginClasspath);
+ }
+
+ /**
+ * Add the possibility to override the ExtensionFinder.
+ */
+ protected ExtensionFinder createExtensionFinder() {
+ return new DefaultExtensionFinder(compoundClassLoader);
+ }
+
+ /**
+ * Add the possibility to override the PluginClassPath.
+ * By default if getRuntimeMode() returns RuntimeMode.DEVELOPMENT than a
+ * DevelopmentPluginClasspath is returned else this method returns
+ * PluginClasspath.
+ */
+ protected PluginClasspath createPluginClasspath() {
+ if (RuntimeMode.DEVELOPMENT.equals(getRuntimeMode())) {
+ return new DevelopmentPluginClasspath();
+ }
+
+ return new PluginClasspath();
+ }
+
+ protected boolean isPluginDisabled(String pluginId) {
+ if (enabledPlugins.isEmpty()) {
+ return disabledPlugins.contains(pluginId);
+ }
+
+ return !enabledPlugins.contains(pluginId);
+ }
+
+ protected FileFilter createHiddenPluginFilter() {
+ return new HiddenFilter();
+ }
+
+ /**
+ * Add the possibility to override the plugins directory.
+ * If a "pf4j.pluginsDir" system property is defined than this method returns
+ * that directory.
+ * If getRuntimeMode() returns RuntimeMode.DEVELOPMENT than a
+ * DEVELOPMENT_PLUGINS_DIRECTORY ("../plugins") is returned else this method returns
+ * DEFAULT_PLUGINS_DIRECTORY ("plugins").
+ * @return
+ */
+ protected File createPluginsDirectory() {
+ String pluginsDir = System.getProperty("pf4j.pluginsDir");
+ if (pluginsDir == null) {
+ if (RuntimeMode.DEVELOPMENT.equals(getRuntimeMode())) {
+ pluginsDir = DEVELOPMENT_PLUGINS_DIRECTORY;
+ } else {
+ pluginsDir = DEFAULT_PLUGINS_DIRECTORY;
+ }
+ }
+
+ return new File(pluginsDir);
+ }
+
+ private void initialize() {
+ plugins = new HashMap<String, PluginWrapper>();
+ pluginClassLoaders = new HashMap<String, PluginClassLoader>();
+ pathToIdMap = new HashMap<String, String>();
+ unresolvedPlugins = new ArrayList<PluginWrapper>();
+ resolvedPlugins = new ArrayList<PluginWrapper>();
+ startedPlugins = new ArrayList<PluginWrapper>();
+ disabledPlugins = new ArrayList<String>();
+ compoundClassLoader = new CompoundClassLoader();
+
+ pluginClasspath = createPluginClasspath();
+ pluginDescriptorFinder = createPluginDescriptorFinder();
+ extensionFinder = createExtensionFinder();
+
+ try {
+ // create a list with plugin identifiers that should be only accepted by this manager (whitelist from plugins/enabled.txt file)
+ enabledPlugins = FileUtils.readLines(new File(pluginsDirectory, "enabled.txt"), true);
+ log.info("Enabled plugins: {}", enabledPlugins);
+
+ // create a list with plugin identifiers that should not be accepted by this manager (blacklist from plugins/disabled.txt file)
+ disabledPlugins = FileUtils.readLines(new File(pluginsDirectory, "disabled.txt"), true);
+ log.info("Disabled plugins: {}", disabledPlugins);
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+
+ System.setProperty("pf4j.pluginsDir", pluginsDirectory.getAbsolutePath());
+ }
+
+ private PluginWrapper loadPluginDirectory(File pluginDirectory) throws PluginException {
+ // try to load the plugin
+ String pluginName = pluginDirectory.getName();
+ String pluginPath = "/".concat(pluginName);
+
+ // test for plugin duplication
+ if (plugins.get(pathToIdMap.get(pluginPath)) != null) {
+ return null;
+ }
+
+ // retrieves the plugin descriptor
+ log.debug("Find plugin descriptor '{}'", pluginPath);
+ PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginDirectory);
+ log.debug("Descriptor " + pluginDescriptor);
+ String pluginClassName = pluginDescriptor.getPluginClass();
+ log.debug("Class '{}' for plugin '{}'", pluginClassName, pluginPath);
+
+ // test for disabled plugin
+ if (isPluginDisabled(pluginDescriptor.getPluginId())) {
+ log.info("Plugin '{}' is disabled", pluginPath);
+ return null;
+ }
+
+ // load plugin
+ log.debug("Loading plugin '{}'", pluginPath);
+ PluginLoader pluginLoader = new PluginLoader(this, pluginDescriptor, pluginDirectory, pluginClasspath);
+ pluginLoader.load();
+ log.debug("Loaded plugin '{}'", pluginPath);
+
+ // create the plugin wrapper
+ log.debug("Creating wrapper for plugin '{}'", pluginPath);
+ PluginWrapper pluginWrapper = new PluginWrapper(pluginDescriptor, pluginPath, pluginLoader.getPluginClassLoader());
+ pluginWrapper.setRuntimeMode(getRuntimeMode());
+ log.debug("Created wrapper '{}' for plugin '{}'", pluginWrapper, pluginPath);
+
+ String pluginId = pluginDescriptor.getPluginId();
+
+ // add plugin to the list with plugins
+ plugins.put(pluginId, pluginWrapper);
+ unresolvedPlugins.add(pluginWrapper);
+
+ // add plugin class loader to the list with class loaders
+ PluginClassLoader pluginClassLoader = pluginLoader.getPluginClassLoader();
+ pluginClassLoaders.put(pluginId, pluginClassLoader);
+
+ return pluginWrapper;
+ }
+
+ private File expandPluginArchive(File pluginArchiveFile) throws IOException {
+ String fileName = pluginArchiveFile.getName();
+ long pluginArchiveDate = pluginArchiveFile.lastModified();
+ String pluginName = fileName.substring(0, fileName.length() - 4);
+ File pluginDirectory = new File(pluginsDirectory, pluginName);
+ // check if exists directory or the '.zip' file is "newer" than directory
+ if (!pluginDirectory.exists() || (pluginArchiveDate > pluginDirectory.lastModified())) {
+ log.debug("Expand plugin archive '{}' in '{}'", pluginArchiveFile, pluginDirectory);
+
+ // do not overwrite an old version, remove it
+ if (pluginDirectory.exists()) {
+ FileUtils.delete(pluginDirectory);
+ }
+
+ // create directory for plugin
+ pluginDirectory.mkdirs();
+
+ // expand '.zip' file
+ Unzip unzip = new Unzip();
+ unzip.setSource(pluginArchiveFile);
+ unzip.setDestination(pluginDirectory);
+ unzip.extract();
+ }
+ return pluginDirectory;
+ }
+
+ private void resolvePlugins() throws PluginException {
+ resolveDependencies();
+ }
+
+ private void resolveDependencies() throws PluginException {
+ DependencyResolver dependencyResolver = new DependencyResolver(unresolvedPlugins);
+ resolvedPlugins = dependencyResolver.getSortedPlugins();
+ for (PluginWrapper pluginWrapper : resolvedPlugins) {
+ unresolvedPlugins.remove(pluginWrapper);
+ compoundClassLoader.addLoader(pluginWrapper.getPluginClassLoader());
+ log.info("Plugin '{}' resolved", pluginWrapper.getDescriptor().getPluginId());
+ }
+ }
+
+}