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

org.dspace.core.LegacyPluginServiceImpl Maven / Gradle / Ivy

There is a newer version: 8.0
Show newest version
/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.core;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.core.service.PluginService;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 

* The Legacy Plugin Service is a very simple component container (based on the * legacy PluginManager class from 5.x or below). It reads defined "plugins" (interfaces) * from config file(s) and makes them available to the API. (TODO: Someday, this * entire "plugin" framework needs to be replaced by Spring Beans.) *

*

* It creates and organizes components (plugins), and helps select a plugin in * the cases where there are many possible choices. It also gives some limited * control over the lifecycle of a plugin. It manages three different types * (usage patterns) of plugins: *

*
    *
  1. Singleton Plugin
    * There is only one implementation class for the plugin. It is indicated * in the configuration. This type of plugin chooses an implementations of * a service, for the entire system, at configuration time. Your * application just fetches the plugin for that interface and gets the * configured-in choice.
  2. * *
  3. Sequence Plugins
    * You need a sequence or series of plugins, to implement a mechanism like * StackableAuthenticationMethods or a pipeline, where each plugin is * called in order to contribute its implementation of a process to the * whole.
  4. *
  5. Named Plugins
    * Use a named plugin when the application has to choose one plugin * implementation out of many available ones. Each implementation is bound * to one or more names (symbolic identifiers) in the configuration.
  6. *
*

* The name is just a String to be associated with the * combination of implementation class and interface. It may contain * any characters except for comma (,) and equals (=). It may contain * embedded spaces. Comma is a special character used to separate * names in the configuration entry. *

* * @author Larry Stone * @author Tim Donohue (turned old PluginManager into a PluginService) * @see SelfNamedPlugin */ public class LegacyPluginServiceImpl implements PluginService { /** * log4j category */ private static final Logger log = LogManager.getLogger(LegacyPluginServiceImpl.class); /** * Prefixes of names of properties to look for in DSpace Configuration */ private static final String SINGLE_PREFIX = "plugin.single."; private static final String SEQUENCE_PREFIX = "plugin.sequence."; private static final String NAMED_PREFIX = "plugin.named."; private static final String SELFNAMED_PREFIX = "plugin.selfnamed."; /** * Configuration name of paths to search for third-party plugins. */ private static final String CLASSPATH = "plugin.classpath"; // Separator character (from perl $;) to make "two dimensional" // hashtable key out of interface classname and plugin name; // this character separates the words. private static final String SEP = "\034"; /** * Paths to search for third-party plugins. */ private String[] classPath; /** * Custom class loader to search for third-party plugins. */ private PathsClassLoader loader; @Autowired(required = true) protected ConfigurationService configurationService; protected LegacyPluginServiceImpl() { } /** * Initialize the bean (after dependency injection has already taken place). * Ensures the configurationService is injected, so that we can load * plugin classpath info from config. * Called by "init-method" in Spring config. */ void init() { String path = configurationService.getProperty(CLASSPATH); if (null == path) { classPath = new String[0]; } else { classPath = path.split(":"); } loader = new PathsClassLoader(LegacyPluginServiceImpl.class.getClassLoader(), classPath); } /** * Returns an instance of the singleton (single) plugin implementing * the given interface. There must be exactly one single plugin * configured for this interface, otherwise the * PluginConfigurationError is thrown. *

* Note that this is the only "get plugin" method which throws an * exception. It is typically used at initialization time to set up * a permanent part of the system so any failure is fatal. * * @param interfaceClass interface Class object * @return instance of plugin * @throws PluginConfigurationError if no matching singleton plugin is configured. */ @Override public Object getSinglePlugin(Class interfaceClass) throws PluginConfigurationError, PluginInstantiationException { String iname = interfaceClass.getName(); // NOTE: module name is ignored, as single plugins ALWAYS begin with SINGLE_PREFIX String key = SINGLE_PREFIX + iname; // configuration format is prefix. = String classname = configurationService.getProperty(key); if (classname != null) { return getAnonymousPlugin(classname.trim()); } else { throw new PluginConfigurationError("No Single Plugin configured for interface \"" + iname + "\""); } } /** * Returns instances of all plugins that implement the interface, * in an Array. Returns an empty array if no there are no * matching plugins. *

* The order of the plugins in the array is the same as their class * names in the configuration's value field. * * @param interfaceClass interface for which to find plugins. * @return an array of plugin instances; if none are * available an empty array is returned. */ @Override public Object[] getPluginSequence(Class interfaceClass) throws PluginInstantiationException { // cache of config data for Sequence Plugins; format its // -> [ .. ] (value is Array) Map sequenceConfig = new HashMap(); // cache the configuration for this interface after grovelling it once: // format is prefix. = String iname = interfaceClass.getName(); String[] classname = null; if (!sequenceConfig.containsKey(iname)) { // NOTE: module name is ignored, as sequence plugins ALWAYS begin with SEQUENCE_PREFIX String key = SEQUENCE_PREFIX + iname; classname = configurationService.getArrayProperty(key); if (classname == null || classname.length == 0) { log.warn("No Configuration entry found for Sequence Plugin interface=" + iname); return (Object[]) Array.newInstance(interfaceClass, 0); } sequenceConfig.put(iname, classname); } else { classname = sequenceConfig.get(iname); } Object result[] = (Object[]) Array.newInstance(interfaceClass, classname.length); for (int i = 0; i < classname.length; ++i) { log.debug("Adding Sequence plugin for interface= " + iname + ", class=" + classname[i]); result[i] = getAnonymousPlugin(classname[i]); } return result; } // Get possibly-cached plugin instance for un-named plugin, // this is shared by Single and Sequence plugins. private Object getAnonymousPlugin(String classname) throws PluginInstantiationException { try { Class pluginClass = Class.forName(classname, true, loader); return pluginClass.getDeclaredConstructor().newInstance(); } catch (ClassNotFoundException e) { throw new PluginInstantiationException("Cannot load plugin class: " + e.toString(), e); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { throw new PluginInstantiationException(e); } } // Map of named plugin classes, [intfc,name] -> class // Also contains intfc -> "marker" to mark when interface has been loaded. private Map namedPluginClasses = new HashMap(); // Map of cached (reusable) named plugin instances, [class,name] -> instance private Map namedInstanceCache = new HashMap(); // load and cache configuration data for the given interface. private void configureNamedPlugin(String iname) throws ClassNotFoundException { int found = 0; /** * First load the class map for this interface (if not done yet): * key is [intfc,name], value is class. * There is ALSO a "marker key" of "intfc" by itself to show we * loaded this intfc's configuration. */ if (!namedPluginClasses.containsKey(iname)) { // 1. Get classes named by the configuration. format is: // plugin.named. = = \, [,] \ // = \, [ ... ] // NOTE: module name is ignored, as named plugins ALWAYS begin with NAMED_PREFIX String key = NAMED_PREFIX + iname; String[] namedVals = configurationService.getArrayProperty(key); if (namedVals != null && namedVals.length > 0) { String prevClassName = null; for (String namedVal : namedVals) { String[] valSplit = namedVal.trim().split("\\s*=\\s*"); String className = null; String name = null; // If there's no "=" separator in this value, assume it's // just a "name" that belongs with previous class. // (This may occur if there's an unescaped comma between names) if (prevClassName != null && valSplit.length == 1) { className = prevClassName; name = valSplit[0]; } else { // first part is class name className = valSplit[0]; prevClassName = className; // second part is one or more names name = valSplit[1]; } // The name may be *multiple* names (separated by escaped commas: \,) String[] names = name.trim().split("\\s*,\\s*"); found += installNamedConfigs(iname, className, names); } } // 2. Get Self-named config entries: // format is plugin.selfnamed. = , .. // NOTE: module name is ignored, as self-named plugins ALWAYS begin with SELFNAMED_PREFIX key = SELFNAMED_PREFIX + iname; String[] selfNamedVals = configurationService.getArrayProperty(key); if (selfNamedVals != null && selfNamedVals.length > 0) { for (String classname : selfNamedVals) { try { Class pluginClass = Class.forName(classname, true, loader); String names[] = (String[]) pluginClass.getMethod("getPluginNames"). invoke(null); if (names == null || names.length == 0) { log.error( "Self-named plugin class \"" + classname + "\" returned null or empty name list!"); } else { found += installNamedConfigs(iname, classname, names); } } catch (NoSuchMethodException e) { log.error( "Implementation Class \"" + classname + "\" is not a subclass of SelfNamedPlugin, it has " + "no getPluginNames() method."); } catch (Exception e) { log.error("Error while configuring self-named plugin", e); } } } namedPluginClasses.put(iname, "org.dspace.core.marker"); if (found == 0) { log.error("No named plugins found for interface=" + iname); } } } // add info for a named plugin to cache, under all its names. private int installNamedConfigs(String iname, String classname, String names[]) throws ClassNotFoundException { int found = 0; for (int i = 0; i < names.length; ++i) { String key = iname + SEP + names[i]; if (namedPluginClasses.containsKey(key)) { log.error("Name collision in named plugin, implementation class=\"" + classname + "\", name=\"" + names[i] + "\""); } else { namedPluginClasses.put(key, classname); } log.debug("Got Named Plugin, intfc=" + iname + ", name=" + names[i] + ", class=" + classname); ++found; } return found; } /** * Returns an instance of a plugin that implements the interface * and is bound to a name matching name. If there is no * matching plugin, it returns null. The names are matched by * String.equals(). * * @param interfaceClass the interface class of the plugin * @param name under which the plugin implementation is configured. * @return instance of plugin implementation, or null if there is no match or an error. */ @Override public Object getNamedPlugin(Class interfaceClass, String name) throws PluginInstantiationException { try { String iname = interfaceClass.getName(); configureNamedPlugin(iname); String key = iname + SEP + name; String cname = namedPluginClasses.get(key); if (cname == null) { log.warn("Cannot find named plugin for interface=" + iname + ", name=\"" + name + "\""); } else { Class pluginClass = Class.forName(cname, true, loader); log.debug("Creating instance of: " + cname + " for interface=" + iname + " pluginName=" + name); Object result = pluginClass.getDeclaredConstructor().newInstance(); if (result instanceof NameAwarePlugin) { ((NameAwarePlugin) result).setPluginInstanceName(name); } return result; } } catch (ClassNotFoundException e) { throw new PluginInstantiationException("Cannot load plugin class: " + e.toString(), e); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { throw new PluginInstantiationException(e); } return null; } @Override public void clearNamedPluginClasses() { namedPluginClasses.clear(); } /** * Returns whether a plugin exists which implements the specified interface * and has a specified name. If a matching plugin is found to be configured, * return true. If there is no matching plugin, return false. * * @param interfaceClass the interface class of the plugin * @param name under which the plugin implementation is configured. * @return true if plugin was found to be configured, false otherwise */ @Override public boolean hasNamedPlugin(Class interfaceClass, String name) throws PluginInstantiationException { try { String iname = interfaceClass.getName(); configureNamedPlugin(iname); String key = iname + SEP + name; return namedPluginClasses.get(key) != null; } catch (ClassNotFoundException e) { throw new PluginInstantiationException("Cannot load plugin class: " + e.toString(), e); } } /** * Returns all of the names under which a named plugin implementing * the interface can be requested (with getNamedPlugin()). * The array is empty if there are no matches. Use this to populate * a menu of plugins for interactive selection, or to document what * the possible choices are. *

* NOTE: The names are NOT returned in any deterministic order. * * @param interfaceClass plugin interface for which to return names. * @return an array of strings with every name; if none are * available an empty array is returned. */ @Override public String[] getAllPluginNames(Class interfaceClass) { try { String iname = interfaceClass.getName(); configureNamedPlugin(iname); String prefix = iname + SEP; ArrayList result = new ArrayList(); for (String key : namedPluginClasses.keySet()) { if (key.startsWith(prefix)) { result.add(key.substring(prefix.length())); } } if (result.size() == 0) { log.error("Cannot find any names for named plugin, interface=" + iname); } return result.toArray(new String[result.size()]); } catch (ClassNotFoundException e) { return new String[0]; } } /* ----------------------------------------------------------------- * Code to check configuration is all below this line * ----------------------------------------------------------------- */ // true if classname is valid and loadable. private boolean checkClassname(String iname, String msg) { try { if (Class.forName(iname, true, loader) != null) { return true; } } catch (ClassNotFoundException ce) { log.error("No class definition found for " + msg + ": \"" + iname + "\""); } return false; } // true if classname is loadable AND is subclass of SelfNamedPlugin private boolean checkSelfNamed(String iname) { try { if (!checkSelfNamed(Class.forName(iname, true, loader))) { log.error("The class \"" + iname + "\" is NOT a subclass of SelfNamedPlugin but it should be!"); } } catch (ClassNotFoundException ce) { log.error("No class definition found for self-named class interface: \"" + iname + "\""); } return false; } // recursively climb superclass stack until we find SelfNamedPlugin private boolean checkSelfNamed(Class cls) { Class sup = cls.getSuperclass(); if (sup == null) { return false; } else if (sup.equals(SelfNamedPlugin.class)) { return true; } else { return checkSelfNamed(sup); } } // check named-plugin names by interface -- call the usual // configuration and let it find missing or duplicate names. private void checkNames(String iname) { try { configureNamedPlugin(iname); } catch (ClassNotFoundException ce) { // bogus classname should be old news by now. } } /** * Validate the entries in the DSpace Configuration relevant to * LegacyPluginServiceImpl. Look for inconsistencies, illegal syntax, etc. * Announce violations with "log.error" so they appear in the log * or in the standard error stream if this is run interactively. *

    *
  • Look for duplicate keys (by parsing the config file) *
  • Interface in plugin.single, plugin.sequence, plugin.named, plugin.selfnamed is valid. *
  • Classname in plugin.reusable exists and matches a plugin config. *
  • Classnames in config values exist. *
  • Classnames in plugin.selfnamed loads and is subclass of SelfNamedPlugin *
  • Implementations of named plugin have no name collisions. *
  • Named plugin entries lacking names. *
* * @throws IOException if IO error */ public void checkConfiguration() throws IOException { FileReader fr = null; BufferedReader cr = null; /* XXX TODO: (maybe) test that implementation class is really a * subclass or impl of the plugin "interface" */ // tables of config keys for each type of config line: Map singleKey = new HashMap(); Map sequenceKey = new HashMap(); Map namedKey = new HashMap(); Map selfnamedKey = new HashMap(); // Find all property keys starting with "plugin." List keys = configurationService.getPropertyKeys("plugin."); for (String key : keys) { if (key.startsWith(SINGLE_PREFIX)) { singleKey.put(key.substring(SINGLE_PREFIX.length()), key); } else if (key.startsWith(SEQUENCE_PREFIX)) { sequenceKey.put(key.substring(SEQUENCE_PREFIX.length()), key); } else if (key.startsWith(NAMED_PREFIX)) { namedKey.put(key.substring(NAMED_PREFIX.length()), key); } else if (key.startsWith(SELFNAMED_PREFIX)) { selfnamedKey.put(key.substring(SELFNAMED_PREFIX.length()), key); } else { log.error("Key with unknown prefix \"" + key + "\" in DSpace configuration"); } } // 2. Build up list of all interfaces and test that they are loadable. // don't bother testing that they are "interface" rather than "class" // since either one will work for the Plugin Manager. ArrayList allInterfaces = new ArrayList(); allInterfaces.addAll(singleKey.keySet()); allInterfaces.addAll(sequenceKey.keySet()); allInterfaces.addAll(namedKey.keySet()); allInterfaces.addAll(selfnamedKey.keySet()); Iterator ii = allInterfaces.iterator(); while (ii.hasNext()) { checkClassname(ii.next(), "key interface or class"); } // Check implementation classes: // - each class is loadable. // - plugin.selfnamed values are each subclass of SelfNamedPlugin // - save classname in allImpls Map allImpls = new HashMap(); // single plugins - just check that it has a valid impl. class ii = singleKey.keySet().iterator(); while (ii.hasNext()) { String key = ii.next(); String val = configurationService.getProperty(SINGLE_PREFIX + key); if (val == null) { log.error("Single plugin config not found for: " + SINGLE_PREFIX + key); } else { val = val.trim(); if (checkClassname(val, "implementation class")) { allImpls.put(val, val); } } } // sequence plugins - all values must be classes ii = sequenceKey.keySet().iterator(); while (ii.hasNext()) { String key = ii.next(); String[] vals = configurationService.getArrayProperty(SEQUENCE_PREFIX + key); if (vals == null || vals.length == 0) { log.error("Sequence plugin config not found for: " + SEQUENCE_PREFIX + key); } else { for (String val : vals) { if (checkClassname(val, "implementation class")) { allImpls.put(val, val); } } } } // 3. self-named plugins - grab and check all values // then make sure it is a subclass of SelfNamedPlugin ii = selfnamedKey.keySet().iterator(); while (ii.hasNext()) { String key = ii.next(); String[] vals = configurationService.getArrayProperty(SELFNAMED_PREFIX + key); if (vals == null || vals.length == 0) { log.error("Selfnamed plugin config not found for: " + SELFNAMED_PREFIX + key); } else { for (String val : vals) { if (checkClassname(val, "selfnamed implementation class")) { allImpls.put(val, val); checkSelfNamed(val); } } checkNames(key); } } // 4. named plugins - extract the classnames and treat same as sequence. // use named plugin config mechanism to test for duplicates, unnamed. ii = namedKey.keySet().iterator(); while (ii.hasNext()) { String key = ii.next(); String[] vals = configurationService.getArrayProperty(NAMED_PREFIX + key); if (vals == null || vals.length == 0) { log.error("Named plugin config not found for: " + NAMED_PREFIX + key); } else { checkNames(key); for (String val : vals) { // each named plugin has two parts to the value, format: // [classname] = [plugin-name] String val_split[] = val.split("\\s*=\\s*"); String classname = val_split[0]; if (checkClassname(classname, "implementation class")) { allImpls.put(classname, classname); } } } } } /** * Invoking this class from the command line just runs * checkConfiguration and shows the results. * There are no command-line options. * * @param argv the command line arguments given * @throws Exception if error */ public void main(String[] argv) throws Exception { checkConfiguration(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy