/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE file at the root of the source
* tree and available online at
*
* https://github.com/keeps/roda
*/
package org.roda.core.plugins;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import org.reflections.Reflections;
import org.roda.core.RodaCoreFactory;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.v2.IsRODAObject;
import org.roda.core.data.v2.ip.AIP;
import org.roda.core.data.v2.ip.DIP;
import org.roda.core.data.v2.ip.File;
import org.roda.core.data.v2.ip.IndexedAIP;
import org.roda.core.data.v2.ip.IndexedDIP;
import org.roda.core.data.v2.ip.IndexedFile;
import org.roda.core.data.v2.ip.IndexedRepresentation;
import org.roda.core.data.v2.ip.Representation;
import org.roda.core.data.v2.jobs.IndexedReport;
import org.roda.core.data.v2.jobs.PluginInfo;
import org.roda.core.data.v2.jobs.PluginType;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.data.v2.risks.IndexedRisk;
import org.roda.core.data.v2.risks.Risk;
import org.roda.core.storage.fs.FSUtils;
import org.roda.core.util.ClassLoaderUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the RODA plugin manager. It is responsible for loading {@link Plugin}
* s.
*
* @author Rui Castro
* @author Hélder Silva <hsilva@keep.pt>
*/
public class PluginManager {
private static final Logger LOGGER = LoggerFactory.getLogger(PluginManager.class);
private static Path RODA_CONFIG_PATH = null;
private static Path RODA_PLUGINS_PATH = null;
private static Path RODA_PLUGINS_SHARED_PATH = null;
private static String RODA_PLUGIN_MANIFEST_KEY = "RODA-Plugin";
private Timer loadPluginsTimer = null;
private Map<Path, JarPlugins> jarPluginCache = new HashMap<>();
private Map<String, Plugin<? extends IsRODAObject>> internalPluginChache = new HashMap<>();
private Map<String, Plugin<? extends IsRODAObject>> externalPluginChache = new HashMap<>();
private Map<PluginType, List<PluginInfo>> pluginInfoPerType = new EnumMap<>(PluginType.class);
private Map<String, Set<Class>> pluginObjectClasses = new HashMap<>();
private Map<Class, List<PluginInfo>> pluginInfoPerObjectClass = new HashMap<>();
private boolean internalPluginStarted = false;
private List<String> blacklistedPlugins;
/**
* The default Plugin Manager instance.
*/
private static PluginManager defaultPluginManager = null;
/**
* Constructs a new {@link PluginManager}.
*
* @throws PluginManagerException
*/
private PluginManager() throws PluginManagerException {
// do nothing
}
/**
* Gets the default {@link PluginManager}.
*
* @return the default {@link PluginManager}.
*
* @throws PluginManagerException
*/
public static synchronized PluginManager instantiatePluginManager(Path rodaConfigPath, Path rodaPluginsPath)
throws PluginManagerException {
if (defaultPluginManager == null) {
RODA_CONFIG_PATH = rodaConfigPath;
RODA_PLUGINS_PATH = rodaPluginsPath;
RODA_PLUGINS_SHARED_PATH = rodaPluginsPath.resolve(RodaConstants.CORE_PLUGINS_SHARED_FOLDER);
defaultPluginManager = new PluginManager();
defaultPluginManager.init();
}
return defaultPluginManager;
}
public static PluginManager getInstance() {
return defaultPluginManager;
}
public <T extends IsRODAObject> void registerPlugin(Plugin<T> plugin) throws PluginException {
try {
plugin.init();
externalPluginChache.put(plugin.getClass().getName(), plugin);
processAndCachePluginInformation(plugin);
LOGGER.debug("Plugin added dynamically started {} (version {})", plugin.getName(), plugin.getVersion());
} catch (Throwable e) {
// 20170123 hsilva: it is required to catch Throwable as there are some
// linking errors that only will happen during the execution (e.g.
// java.lang.NoSuchMethodError)
throw new PluginException("An exception have occured during plugin registration", e);
}
}
/**
* Returns all {@link Plugin}s present in all jars.
*
* @return a {@link List} of {@link Plugin}s.
*/
public List<Plugin<? extends IsRODAObject>> getPlugins() {
List<Plugin<? extends IsRODAObject>> plugins = new ArrayList<>();
plugins.addAll(internalPluginChache.values());
plugins.addAll(externalPluginChache.values());
return plugins;
}
/**
* Returns the {@link PluginInfo}s for all {@link Plugin}s.
*
* @return a {@link List} of {@link PluginInfo}s.
*/
public List<PluginInfo> getPluginsInfo() {
return getPlugins().stream().map(e -> getPluginInfo(e)).collect(Collectors.toList());
}
public List<PluginInfo> getPluginsInfo(PluginType pluginType) {
return pluginInfoPerType.get(pluginType);
}
public List<PluginInfo> getPluginsInfo(List<PluginType> pluginTypes) {
List<PluginInfo> pluginsInfo = new ArrayList<>();
for (PluginType pluginType : pluginTypes) {
pluginsInfo.addAll(pluginInfoPerType.getOrDefault(pluginType, Collections.emptyList()));
}
return pluginsInfo;
}
public Map<String, Set<Class>> getPluginObjectClasses() {
return pluginObjectClasses;
}
public Set<Class> getPluginObjectClasses(String pluginID) {
return pluginObjectClasses.get(pluginID);
}
public <T extends IsRODAObject> Set<Class> getPluginObjectClasses(Plugin<T> plugin) {
return pluginObjectClasses.get(plugin.getClass().getName());
}
public Map<Class, List<PluginInfo>> getPluginInfoPerObjectClass() {
return pluginInfoPerObjectClass;
}
public List<PluginInfo> getPluginInfoPerObjectClass(Class clazz) {
return pluginInfoPerObjectClass.get(clazz);
}
public List<PluginInfo> getPluginInfoPerObjectClass(String className) {
try {
return pluginInfoPerObjectClass.get(Class.forName(className));
} catch (ClassNotFoundException e) {
return new ArrayList<>();
}
}
/**
* Returns an instance of the {@link Plugin} with the specified ID
* (classname).
*
* @param pluginID
* the ID (classname) of the {@link Plugin}.
*
* @return a {@link Plugin} or <code>null</code> if the specified classname is
* not a {@link Plugin} or something went wrong during its init().
*/
public Plugin<? extends IsRODAObject> getPlugin(String pluginID) {
Plugin<? extends IsRODAObject> plugin = null;
Plugin<? extends IsRODAObject> cachedInternalPlugin = internalPluginChache.get(pluginID);
if (cachedInternalPlugin != null) {
plugin = cachedInternalPlugin.cloneMe();
}
boolean internalPluginTakesPrecedence = RodaCoreFactory.getRodaConfiguration()
.getBoolean("core.plugins.internal.take_precedence_over_external");
Plugin<? extends IsRODAObject> cachedExternalPlugin = externalPluginChache.get(pluginID);
if ((plugin == null || !internalPluginTakesPrecedence) && cachedExternalPlugin != null) {
plugin = cachedExternalPlugin.cloneMe();
}
return plugin;
}
public <T extends IsRODAObject> Plugin<T> getPlugin(String pluginID, Class<T> pluginClass) {
return (Plugin<T>) getPlugin(pluginID);
}
/**
* @param pluginID
*
* @return {@link PluginInfo} or <code>null</code>.
*/
public PluginInfo getPluginInfo(String pluginID) {
Plugin<? extends IsRODAObject> plugin = getPlugin(pluginID);
if (plugin != null) {
return getPluginInfo(plugin);
} else {
return null;
}
}
/**
* This method should be called to stop {@link PluginManager} and all
* {@link Plugin}s currently loaded.
*/
public void shutdown() {
if (this.loadPluginsTimer != null) {
// Stop the plugin loader timer
this.loadPluginsTimer.cancel();
}
for (JarPlugins jarPlugins : this.jarPluginCache.values()) {
for (Plugin<? extends IsRODAObject> plugin : jarPlugins.plugins) {
if (plugin != null) {
plugin.shutdown();
}
}
}
}
private void init() {
// load, for the first time, all the plugins (internal & external)
loadPlugins();
// schedule
LOGGER.debug("Starting plugin scanner timer...");
int timeInSeconds = RodaCoreFactory.getRodaConfiguration().getInt("core.plugins.external.scheduler.interval");
this.loadPluginsTimer = new Timer("Plugin scanner timer", true);
this.loadPluginsTimer.schedule(new SearchPluginsTask(), timeInSeconds * 1000, timeInSeconds * 1000);
LOGGER.info("{} init OK", getClass().getSimpleName());
}
private <T extends IsRODAObject> PluginInfo getPluginInfo(Plugin<T> plugin) {
return new PluginInfo(plugin.getClass().getName(), plugin.getName(), plugin.getVersion(), plugin.getDescription(),
plugin.getType(), plugin.getCategories(), plugin.getParameters());
}
private void loadPlugins() {
// reload backlisted plugins
blacklistedPlugins = RodaCoreFactory.getRodaConfigurationAsList("core", "plugins", "blacklist");
// load "external" RODA plugins, i.e., those available in the plugins folder
if (FSUtils.exists(RODA_PLUGINS_PATH) && FSUtils.isDirectory(RODA_PLUGINS_PATH)) {
loadExternalPlugins();
}
// load internal RODA plugins
if (!internalPluginStarted) {
loadInternalPlugins();
}
}
private void loadExternalPlugins() {
try {
// load shared jars
List<URL> sharedJarURLs = getSharedJarURLs(RODA_PLUGINS_SHARED_PATH);
// lets warn about jars that will not be loaded
try (DirectoryStream<Path> stream = Files.newDirectoryStream(RODA_PLUGINS_PATH, "*.jar")) {
Iterator<Path> iterator = stream.iterator();
if (iterator.hasNext()) {
LOGGER.error(
"'{}' has jars that will not be loaded as they are expected inside a folder (don't use folder '{}' to put them if you're not "
+ "100% sure that they should be used when loading each plugin. Instead, consider putting them inside folder '{}' to remove "
+ "this error)! And the jars are:",
RODA_PLUGINS_PATH, RodaConstants.CORE_PLUGINS_SHARED_FOLDER, RodaConstants.CORE_PLUGINS_DISABLED_FOLDER);
iterator.forEachRemaining(path -> LOGGER.error(" {}", path));
}
}
// process each folder inside the plugins folder (except shared &
// disabled)
try (DirectoryStream<Path> pluginsFolders = Files.newDirectoryStream(RODA_PLUGINS_PATH,
path -> Files.isDirectory(path)
&& !RodaConstants.CORE_PLUGINS_SHARED_FOLDER.equals(path.getFileName().toString())
&& !RodaConstants.CORE_PLUGINS_DISABLED_FOLDER.equals(path.getFileName().toString()))) {
for (Path pluginFolder : pluginsFolders) {
LOGGER.debug("Processing plugin folder '{}'", pluginFolder);
List<Path> pluginJarFiles = new ArrayList<>();
List<URL> pluginJarURLs = new ArrayList<>();
try (DirectoryStream<Path> jarsStream = Files.newDirectoryStream(pluginFolder, "*.jar")) {
for (Path jarFile : jarsStream) {
LOGGER.debug(" Will process jar '{}'", jarFile);
pluginJarFiles.add(jarFile);
pluginJarURLs.add(jarFile.toUri().toURL());
}
} catch (NoSuchFileException e) {
// do nothing as folder does not exist
}
pluginJarURLs.addAll(sharedJarURLs);
URL[] jars = pluginJarURLs.toArray(new URL[pluginJarURLs.size()]);
for (Path jarFile : pluginJarFiles) {
processJar(jarFile, jars);
}
}
}
} catch (IOException e) {
LOGGER.error("Error while instantiating external plugins", e);
}
}
private List<URL> getSharedJarURLs(Path folder) throws IOException {
List<URL> sharedJarURLs = new ArrayList<>();
try (DirectoryStream<Path> sharedStream = Files.newDirectoryStream(folder, "*.jar")) {
for (Path jarFile : sharedStream) {
sharedJarURLs.add(jarFile.toUri().toURL());
}
} catch (NoSuchFileException e) {
// do nothing as folder does not exist
}
return sharedJarURLs;
}
private void processJar(Path jarFile, URL[] jars) throws IOException {
BasicFileAttributes attrs = Files.readAttributes(jarFile, BasicFileAttributes.class);
if (jarPluginCache.containsKey(jarFile)
&& attrs.lastModifiedTime().toMillis() == jarPluginCache.get(jarFile).lastModified) {
LOGGER.debug("{} is already loaded", jarFile.getFileName());
} else {
// The plugin doesn't exist or the modification date is
// different. Let's load the Plugin
List<Plugin<? extends IsRODAObject>> plugins = loadPlugin(jarFile, jars);
if (!plugins.isEmpty()) {
LOGGER.info("'{}' (is new? {}) is not loaded or modification dates differ. Inspecting Jar...",
jarFile.getFileName(), jarPluginCache.containsKey(jarFile));
}
for (Plugin<? extends IsRODAObject> plugin : plugins) {
try {
if (plugin != null && !blacklistedPlugins.contains(plugin.getClass().getName())) {
plugin.init();
externalPluginChache.put(plugin.getClass().getName(), plugin);
processAndCachePluginInformation(plugin);
LOGGER.info("Plugin started '{}' (version {})", plugin.getName(), plugin.getVersion());
} else {
LOGGER.trace("'{}' is not a Plugin", jarFile.getFileName());
}
synchronized (jarPluginCache) {
if (jarPluginCache.get(jarFile) != null) {
JarPlugins jarPlugins = jarPluginCache.get(jarFile);
jarPlugins.plugins = new ArrayList<>();
jarPlugins.plugins.add(plugin);
jarPlugins.lastModified = attrs.lastModifiedTime().toMillis();
} else {
jarPluginCache.put(jarFile, new JarPlugins(plugin, attrs.lastModifiedTime().toMillis()));
}
}
} catch (Exception | LinkageError e) {
LOGGER.error("Plugin failed to initialize: {}", jarFile, e);
}
}
}
}
@SuppressWarnings("rawtypes")
private void loadInternalPlugins() {
Reflections reflections = new Reflections(
RodaCoreFactory.getRodaConfigurationAsString("core", "plugins", "internal", "package"));
Set<Class<? extends AbstractPlugin>> plugins = reflections.getSubTypesOf(AbstractPlugin.class);
plugins.addAll(reflections.getSubTypesOf(AbstractAIPComponentsPlugin.class));
for (Class<? extends AbstractPlugin> plugin : plugins) {
String name = plugin.getName();
if (!Modifier.isAbstract(plugin.getModifiers()) && !blacklistedPlugins.contains(name)) {
LOGGER.debug("Loading internal plugin '{}'", name);
try {
Plugin<? extends IsRODAObject> p = (Plugin<?>) ClassLoaderUtility.createObject(plugin.getName());
p.init();
internalPluginChache.put(plugin.getName(), p);
processAndCachePluginInformation(p);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | PluginException
| RuntimeException e) {
LOGGER.error("Unable to instantiate plugin '{}'", plugin.getName(), e);
}
}
}
internalPluginStarted = true;
}
private <T extends IsRODAObject> void processAndCachePluginInformation(Plugin<T> plugin) {
// cache plugin > objectClasses
Set<Class> objectClasses = new HashSet<>(plugin.getObjectClasses());
if (objectClasses.contains(AIP.class)) {
objectClasses.add(IndexedAIP.class);
} else if (objectClasses.contains(IndexedAIP.class)) {
objectClasses.add(AIP.class);
}
if (objectClasses.contains(Representation.class)) {
objectClasses.add(IndexedRepresentation.class);
} else if (objectClasses.contains(IndexedRepresentation.class)) {
objectClasses.add(Representation.class);
}
if (objectClasses.contains(File.class)) {
objectClasses.add(IndexedFile.class);
} else if (objectClasses.contains(IndexedFile.class)) {
objectClasses.add(File.class);
}
if (objectClasses.contains(Risk.class)) {
objectClasses.add(IndexedRisk.class);
} else if (objectClasses.contains(IndexedRisk.class)) {
objectClasses.add(Risk.class);
}
if (objectClasses.contains(DIP.class)) {
objectClasses.add(IndexedDIP.class);
} else if (objectClasses.contains(IndexedDIP.class)) {
objectClasses.add(DIP.class);
}
if (objectClasses.contains(Report.class)) {
objectClasses.add(IndexedReport.class);
} else if (objectClasses.contains(IndexedReport.class)) {
objectClasses.add(Report.class);
}
pluginObjectClasses.put(plugin.getClass().getName(), objectClasses);
// cache plugintype > plugininfos
PluginInfo pluginInfo = getPluginInfo(plugin.getClass().getName());
objectClasses.stream().forEach(objectClass -> pluginInfo.addObjectClass(objectClass.getName()));
PluginType pluginType = plugin.getType();
if (pluginInfoPerType.get(pluginType) == null) {
List<PluginInfo> list = new ArrayList<>();
list.add(pluginInfo);
pluginInfoPerType.put(pluginType, list);
} else if (!pluginInfoPerType.get(pluginType).contains(pluginInfo)) {
pluginInfoPerType.get(pluginType).add(pluginInfo);
}
// cache objectClass > plugininfos
for (Class class1 : getPluginObjectClasses(plugin)) {
if (pluginInfoPerObjectClass.get(class1) == null) {
List<PluginInfo> list = new ArrayList<>();
list.add(pluginInfo);
pluginInfoPerObjectClass.put(class1, list);
} else {
pluginInfoPerObjectClass.get(class1).add(pluginInfo);
}
}
}
private List<Plugin<?>> loadPlugin(Path jarFile, URL[] jars) {
List<Plugin<?>> ret = new ArrayList<>();
Plugin<?> plugin = null;
try (JarFile jar = new JarFile(jarFile.toFile())) {
Manifest manifest = jar.getManifest();
if (manifest == null) {
LOGGER.trace("{} doesn't have a MANIFEST file", jarFile.getFileName());
} else {
Attributes mainAttributes = manifest.getMainAttributes();
String pluginClassNames = mainAttributes.getValue(RODA_PLUGIN_MANIFEST_KEY);
if (pluginClassNames != null) {
for (String pluginClassName : pluginClassNames.split("\\s+")) {
LOGGER.trace("{} has plugin {}", jarFile.getFileName(), pluginClassName);
LOGGER.trace("Adding jar {} to classpath and loading {} with ClassLoader {}", jarFile.getFileName(),
pluginClassName, URLClassLoader.class.getSimpleName());
Object object = ClassLoaderUtility.createObject(jars, pluginClassName);
if (Plugin.class.isAssignableFrom(object.getClass())) {
plugin = (Plugin<?>) object;
ret.add(plugin);
} else {
LOGGER.error("{} is not a valid Plugin", pluginClassNames);
}
}
} else {
LOGGER.trace("{} MANIFEST file doesn't have a '{}' attribute", jarFile.getFileName(),
RODA_PLUGIN_MANIFEST_KEY);
}
}
} catch (IOException | ClassNotFoundException | NoClassDefFoundError | InstantiationException
| IllegalAccessException | RuntimeException e) {
LOGGER.error("Error loading plugin from {}", jarFile.getFileName(), e);
}
return ret;
}
protected class SearchPluginsTask extends TimerTask {
@Override
public void run() {
LOGGER.debug("Searching for plugins...");
loadPlugins();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Search complete - {} jar files", jarPluginCache.size());
for (Entry<Path, JarPlugins> jarEntry : jarPluginCache.entrySet()) {
Path jarFile = jarEntry.getKey();
List<Plugin<?>> plugins = jarEntry.getValue().plugins;
if (!plugins.isEmpty()) {
for (Plugin<?> plugin : plugins) {
LOGGER.debug("- {}", jarFile.getFileName());
LOGGER.debug("--- {} - {} - {}", plugin.getName(), plugin.getVersion(), plugin.getDescription());
}
}
}
}
}
}
protected class JarPlugins {
protected List<Plugin<?>> plugins = new ArrayList<>();
private long lastModified = 0;
JarPlugins(Plugin<?> plugin, long lastModified) {
plugins.add(plugin);
this.lastModified = lastModified;
}
}
}