PluginFactory.java
package com.github.celldynamics.quimp.plugin.engine;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
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.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.celldynamics.quimp.QuimP;
import com.github.celldynamics.quimp.plugin.IQuimpCorePlugin;
/*
* //!>
* @startuml doc-files/PluginFactory_1_UML.png
* actor user
* participant PluginFactory as PF
* participant Plugin as PL
* == Create instance of PluginFactory ==
* user -> PF : //<<create>>//
* activate PF
* PF -> PF : init ""availPlugins""
* PF -> PF : scanDirectory()
* activate PF
* PF -> PF : discover qname getClassName
* PF -> PF : getPluginType()
* activate PF
* PF -> PL : //<<getPluginInstance>>//
* activate PL
* PF -> PL : getPluginType()
* PL --> PF : ""type""
* PF -> PL : getPluginVersion()
* PL --> PF : ""version""
* destroy PL
* PF -> PF : store at ""availPlugins""
* deactivate PF
* deactivate PF
* == Get names ==
* user -> PF : getPluginNames(type)
* loop ""availPlugins""
* PF -> PF : check ""type""
* end
* PF --> user : List
* == Get Instance ==
* user -> PF : getInstance(name)
* PF -> PF : find plugin
* PF -> PL : //<<getPluginInstance>>//
* activate PL
* PF --> user : ""instance""
* note left
* Only one instance of requested
* plugin is created.
* On next requests previous reference
* is returned
* endnote
* @enduml
*
* @startuml doc-files/PluginFactory_2_UML.png
* partition PluginFactory(directory) {
* (*) --> if "plugin directory\n exists" then
* -->[true] init ""availPlugins""
* --> "scanDirectory()"
* -right-> (*)
* else
* -->[false] "throw QuimpPluginException"
* --> (*)
* endif
* }
* @enduml
*
* @startuml doc-files/PluginFactory_3_UML.png
* partition scanDirectory() {
* (*) --> Get file \nfrom ""root""
* if "file contains\n**-quimp.jar**" then
* -->[true] Discover qualified name
* --> getPluginType()
* --> if Type valid\njar valid\nreadable then
* -->[true] Store at ""availPlugins""
* --> Get file \nfrom ""root""
* else
* -->[false] log error
* --> Get file \nfrom ""root""
* endif
* else
* -->[false] Get file \nfrom ""root""
* endif
* }
* @enduml
*
* @startuml doc-files/PluginFactory_4_UML.png
* start
* :call ""setup()"" from jar;
* if (valid plugin type?) then (true)
* :Return plugin type;
* stop
* else (false)
* :throw Exception;
* endif
* end
* @enduml
*
* @startuml doc-files/PluginFactory_5_UML.png
* start
* :Load jar;
* :Create instance;
* end
* @enduml
*
* @startuml doc-files/PluginFactory_6_UML.png
* start
* if (name is not empty) then (yes)
* :Build qualified name\nfrom ""getClassName()"";
* if (get plugin data from\n ""availPlugins"") then (null)
* :log error;
* ->Return null;
* stop
* else
* if (was plugin used\nbefore) then (yes)
* :Restore instance;
* ->Return instance;
* stop
* else
* :""getPluginInstance"";
* :Store instance;
* endif
* endif
* else
* stop
* endif
* stop
* @enduml
* //!<
*/
/**
* Plugin jar loader.
*
* <p>Created object is connected with directory where plugins exist. This directory is scanned for
* jar
* files that meet given below naming conventions. Every file that meets naming convention is loaded
* and asked for method IQuimpPlugin.setup(). On success the plugin is registered in availPlugins
* database:
*
* {@code <Name, <File, Type, ClassName>>}
*
* <p>Where Name is name of plugin extracted form file name (see below required naming conventions),
* File is handle to file on disk, Type is type of plugin according to types defined in
* warwick.wsbc.plugin.IQuimpPlugin and ClassName is qualified name of class of plugin. The
* ClassName is extracted from the plugin jar file assuming that plugin class contains underscore _
* in its name. If more classes underscored is found in jar, only the first discovered is loaded.
* Thus the following conventions are required:
* <ol>
* <li>Plugin name must contain <b>-quimp</b> to be considered as plugin (see PATTERN field)
* <li>Class name in plugin must end with underscore to be considered as plugin main class
* </ol>
*
* <p>Simplified sequence diagrams are as follows: <br>
* <img src="doc-files/PluginFactory_1_UML.png"/><br>
*
* <p>This class try to hide all exceptions that can be thrown during loading plugins from user. In
* general only when user pass wrong path to plugins directory exception is thrown. In all other
* cases class returns null pointers or empty lists. Error handling:
* <ol>
* <li>Given directory exists but there is no plugins inside
* <ol>
* <li>getPluginNames(int) returns empty list (length 0)
* </ol>
* <li>Given directory exists but plugins are corrupted - they fulfil naming criterion but they are
* not valid QuimP plugins
* <ol>
* <li>getInstance(final String) returns <tt>null</tt> when correct name is given. It means that
* plugin has
* been registered by scanDirectory() so it had correct name and supported
* wsbc.plugin.IQuimpPlugin.setup() method
* </ol>
* <li>Given directory does not exist
* <ol>
* <li>Constructor throws QuimpPluginException
* </ol>
* <li>User asked for unknown name in getInstance(final String)
* <ol>
* <li>getInstance(final String) return null
* </ol>
* </ol>
* Internally getPluginType(final File, final String) and getInstance(final String) throw exceptions
* around class loading and running methods from them. Additionally getPluginType(final File, final
* String) throws exception when unknown type is returned from valid plugin. These exceptions are
* caught preventing adding that plugin into availPlugins database (scanDirectory()) or hidden in
* getInstance that returns null in this case. All exceptions are masked besides scanDirectory()
* that can throw checked PluginException that must be handled by caller. It usually means that
* given plugin directory does not exist.
*
* <p>Each jar is loaded only once on first request. Next requests get the same instance.
*
* @author p.baniukiewicz
*/
public class PluginFactory {
/**
* The Constant LOGGER.
*/
static final Logger LOGGER = LoggerFactory.getLogger(PluginFactory.class.getName());
/**
* Name pattern of plugins.
*/
private static final String PATTERN = "-quimp";
/**
* List of plugins found in initial directory path passed to constructor.
*
* <p>Plugins are organized in list [name, [path, qname, type]] where:
* <ol>
* <li>name is the name of plugin extracted from plugin jar filename. Name is always encoded as
* Name - starts with capital letter
* <li>path is full path with jar filename
* <li>qname is qualified name of plugin class obtained from jar name
* <li>type is type of plugin read from IQuimpPlugin.setup() method
* </ol>
*
* <p>This field is set by scanDirectory() method -> getPluginType()
*/
private HashMap<String, PluginProperties> availPlugins;
private Path root;
/**
* Accessor to internal database of loaded plugins.
*
* @return Non-modifiable database of loaded plugins
*/
public Map<String, PluginProperties> getRegisterdPlugins() {
return Collections.unmodifiableMap(availPlugins);
}
/**
* Build object connected to plugin directory.
*
* <p>Can throw exception if there is no directory path. <br>
* <img src="doc-files/PluginFactory_2_UML.png"/><br>
*
* @param path
*
*/
public PluginFactory(final Path path) {
if (QuimP.SUPER_DEBUG) {
getSystemClassPath();
}
LOGGER.debug("Attached " + path.toString());
availPlugins = new HashMap<String, PluginProperties>();
// check if dir exists
if (Files.notExists(path)) {
LOGGER.warn("Plugin directory can not be read");
root = Paths.get("/");
} else {
root = path;
scanDirectory();
}
}
/**
* Scan path for files that match PATTERN name and end with .jar.
*
* <p>Fill availPlugins field. Field name is filled as Name without dependency how original
* filename was written. It is converted to small letters and then first char is upper-case
* written. <br>
* <img src="doc-files/PluginFactory_3_UML.png"/><br>
*
* @return table of files that fulfill criterion:
* <ol>
* <li>have extension
* <li>extension is .jar or .JAR
* <li>contain PATTERN in name
* </ol>
* If there is no plugins in directory it returns 0 length array
*/
private File[] scanDirectory() {
File fi = new File(root.toString());
File[] listFiles = fi.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, final String name) {
String sname = name.toLowerCase();
if (sname.lastIndexOf('.') <= 0) {
return false; // no extension
}
int lastIndex = sname.lastIndexOf('.');
// get extension
String ext = sname.substring(lastIndex);
if (!ext.equals(".jar")) {
return false; // no jar extension
}
// now we have .jar file, check name pattern
if (sname.contains(PATTERN)) {
return true;
} else {
return false;
}
}
});
if (listFiles == null) {
return new File[0]; // but if yes return empty array
}
// decode names from listFiles and fill availPlugins names and paths
for (File f : listFiles) {
// build plugin name from file name
String filename = f.getName().toLowerCase();
int lastindex = filename.lastIndexOf(PATTERN);
// cut from beginning to -quimp
String pluginName = filename.substring(0, lastindex);
// change first letter to upper to match class-naming convention
pluginName = pluginName.substring(0, 1).toUpperCase() + pluginName.substring(1);
// check plugin type
try {
// ask for class names in jar
String cname = getClassName(f);
// make temporary instance
Object inst = getPluginInstance(f, cname);
// get type of path.classname plugin
int type = getPluginType(inst);
// get version of path.classname plugin
String ver = getPluginVersion(inst);
// create entry with classname and path
availPlugins.put(pluginName, new PluginProperties(f, cname, type, ver));
LOGGER.debug(
"Registered plugin: " + pluginName + " " + availPlugins.get(pluginName).toString());
// catch any error in plugin services - plugin is not stored
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException
| InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | ClassCastException | IOException
| NoClassDefFoundError e) {
LOGGER.error("Type of plugin " + pluginName + " in jar: " + f.getPath()
+ " can not be obtained. Ignoring this plugin");
LOGGER.debug(e.getMessage(), e);
}
}
return Arrays.copyOf(listFiles, listFiles.length);
}
/**
* Extracts qualified name of classes in jar file. Class name must contain underscore.
*
* @param pathToJar path to jar file
* @return Name of first discovered class with underscore
* @throws IOException When jar can not be opened
* @throws IllegalArgumentException when there is no classes in jar
* @see <a href=
* "link">http://stackoverflow.com/questions/11016092/how-to-load-classes-at-runtime-from-a-folder-or-jar</a>
*/
private String getClassName(File pathToJar) throws IOException {
ArrayList<String> names = new ArrayList<>(); // all discovered names
JarFile jarFile = new JarFile(pathToJar);
Enumeration<JarEntry> e = jarFile.entries();
while (e.hasMoreElements()) {
JarEntry je = (JarEntry) e.nextElement();
String entryname = je.getName();
if (je.isDirectory() || !entryname.endsWith("_.class")) {
continue;
}
// -6 because of .class
String className = je.getName().substring(0, je.getName().length() - 6);
className = className.replace('/', '.');
names.add(className);
LOGGER.debug("In " + pathToJar.toString() + " found class " + entryname);
}
jarFile.close();
if (names.isEmpty()) {
throw new IllegalArgumentException("getClassName: There is no underscored classes in jar");
}
if (names.size() > 1) {
LOGGER.warn("More than one underscored class in jar " + pathToJar.toString()
+ " Take first one " + names.get(0));
}
return names.get(0);
}
/**
* Gets type of plugin.
*
* <p>Calls IQuimpPlugin.setup() method from plugin. <br>
* <img src="doc-files/PluginFactory_4_UML.png"/><br>
*
* @param instance Instance of plugin
* @return Codes of types from IQuimpPlugin
* @throws IllegalArgumentException When returned type is unknown
* @throws NoSuchMethodException wrong plugin
* @throws InvocationTargetException wrong plugin
* @see com.github.celldynamics.quimp.plugin.IQuimpCorePlugin
*/
private int getPluginType(Object instance)
throws IllegalArgumentException, NoSuchMethodException, InvocationTargetException {
int result = (int) ((IQuimpCorePlugin) instance).setup();
// decode returned result for plugin type
if ((result & IQuimpCorePlugin.DOES_SNAKES) == IQuimpCorePlugin.DOES_SNAKES) {
return IQuimpCorePlugin.DOES_SNAKES;
} else {
throw new IllegalArgumentException("Plugin returned unknown type");
}
}
/**
* Gets version of plugin.
*
* <p>Calls IQuimpPlugin.getVersion() method from plugin
*
* @param instance Instance of plugin
* @return String representing version of plugin or null if plugin does not support versioning
* @throws NoSuchMethodException wrong plugin
* @throws InvocationTargetException wrong plugin
*/
private String getPluginVersion(Object instance)
throws NoSuchMethodException, InvocationTargetException {
return ((IQuimpCorePlugin) instance).getVersion();
}
/**
* Creates instance of plugin. <br>
* <img src="doc-files/PluginFactory_5_UML.png"/><br>
*
* @param plugin plugin File handler to plugin
* @param className full class name
* @return className Formatted fully qualified class name
* @throws InstantiationException wrong plugin
* @throws IllegalAccessException wrong plugin
* @throws ClassNotFoundException wrong plugin
* @throws MalformedURLException wrong plugin
*/
private Object getPluginInstance(final File plugin, final String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException,
MalformedURLException {
URL[] url = new URL[] { plugin.toURI().toURL() };
ClassLoader child = new URLClassLoader(url);
LOGGER.trace("Trying to load class: " + className + " from " + plugin.toString());
Class<?> classToLoad = Class.forName(className, true, child);
Object instance = classToLoad.newInstance();
return instance;
}
/**
* Return list of plugins of given types.
*
* @param type Type defined in com.github.celldynamics.plugin.IQuimpPlugin
* @return List of names of plugins of type type. If there is no plugins in directory (this type
* or any) returned list has length 0
*/
public ArrayList<String> getPluginNames(int type) {
ArrayList<String> ret = new ArrayList<String>();
// Iterate over our collection
Iterator<Map.Entry<String, PluginProperties>> it = availPlugins.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, PluginProperties> me = it.next();
if (me.getValue().getType() == type) {
ret.add(me.getKey()); // add to list of plugins of this type
}
}
if (ret.isEmpty()) {
LOGGER.warn("No plugins found");
}
return ret;
}
/**
* Return instance of named plugin.
*
* <p><br>
* <img src="doc-files/PluginFactory_6_UML.png"/><br>
*
* <p>JAR is loaded only once on first request. Then its reference is stored in
* {@link PluginProperties} and served on next demand. Thus, if plugin is used on different frames
* in stack it is the same instance.
*
* @param name Name of plugin compatible with general rules
* @return reference to plugin of name or null when there is any problem with creating instance
* or given name does not exist in availPlugins base
*/
public IQuimpCorePlugin getInstance(final String name) {
try {
if (name.isEmpty()) {
throw new IllegalArgumentException("Plugin of name: " + name + " is not loaded");
}
// usually name of plugin is spelled with Capital letter first
// make sure that name is in correct format
String qname = name.substring(0, 1).toUpperCase() + name.substring(1);
// find name in database
PluginProperties pp = availPlugins.get(qname);
if (pp == null) {
throw new IllegalArgumentException("Plugin of name: " + name + " is not loaded");
}
// load class and create instance
IQuimpCorePlugin instance;
if (pp.getRef() == null) { // not used yet
instance = (IQuimpCorePlugin) getPluginInstance(pp.getFile(), pp.getClassName());
pp.setRef(instance); // store for next request
} else {
instance = pp.getRef(); // return same instance as previous for "name"
}
return instance;
} catch (MalformedURLException | ClassNotFoundException | InstantiationException
| IllegalAccessException | IllegalArgumentException e) {
LOGGER.error("Plugin " + name + " can not be instanced (reason: " + e.getMessage() + ")");
LOGGER.debug(e.getMessage(), e);
return null;
}
}
/**
* Prints system classpath.
*/
public static void getSystemClassPath() {
LOGGER.trace("--- CLASPATH ---");
ClassLoader cl = ClassLoader.getSystemClassLoader();
URL[] urls = ((URLClassLoader) cl).getURLs();
for (URL urll : urls) {
LOGGER.trace(urll.getFile());
}
LOGGER.trace("--- CLASPATH ---");
}
}