package org.signalml.plugin.loader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URLConnection;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.log4j.Logger;
import org.jfree.ui.FilesystemFilter;
import org.apache.log4j.Logger;
import org.signalml.app.view.workspace.ViewerElementManager;
import org.signalml.plugin.export.Plugin;
import org.signalml.plugin.impl.PluginAccessClass;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Class responsible for loading plug-ins (high level). Its main functions are:
* <ul>
* <li>to read (and write when the application is closing) the list of
* directories in which the plug-ins are stored,</li>
* <li>to read (and write when the application is closing) the
* {@link PluginState states} (whether they should be active or not) of
* plug-ins,</li>
* <li>to read {@link PluginDescription descriptions} of plug-ins and check
* if their {@link PluginDependency dependencies} are satisfied,</li>
* <li>to load active plug-ins using {@link PluginLoaderLo class loader},</li>
* <li>to create the {@link PluginDialog dialog} to manage plug-in options.
* </li>
* </ul>
*
* @author Marcin Szumski
* @author Stanislaw Findeisen (Eisenbits)
*/
public class PluginLoaderHi {
private final Logger logger = Logger.getLogger(PluginLoaderHi.class);
/**
* the path to the XML file with {@link PluginState states} of
* plug-ins
*/
private final File pluginsStateFile;
/**
* the path to the XML file with the list of directories where
* plug-ins are stored
*/
private final File pluginsDirectoriesFile;
//constants for reading XML files
private static final String XMLDirectoryNode = "directory";
private static final String XMLDirectoriesNode = "directories";
private static final String XMLStatesPluginsNode = "plugins";
private static final String XMLStatesPluginNode = "plugin";
private static final String XMLStatesPluginNameNode = "name";
private static final String XMLStatesPluginActiveNode = "active";
/**
* the shared (only) instance of this loader
*/
private static PluginLoaderHi sharedInstance = null;
/**
* the list of directories in which plug-ins are stored
*/
private ArrayList<File> pluginDirs = new ArrayList<File>();
/**
* the directory where default plug-ins are stored
*/
private ArrayList<File> globalPluginDirectories = new ArrayList<File>();
/**
* list of descriptions of plug-ins.
* On this list are only plug-ins that has an XML description
* file in one of the {@link #pluginDirs directories}.
*/
private ArrayList<PluginDescription> descriptions = new ArrayList<PluginDescription>();
/**
* HashMap that allows to access pluginDescription by the name
* of the plug-in
*/
private HashMap<String, PluginDescription> descriptionsByName = new HashMap<String, PluginDescription>();
/**
* list of {@link PluginState states} of plug-ins.
* On this list are states stored in an {@link #pluginsStateFile XML file}
* with plug-in states.
*/
private ArrayList<PluginState> states = new ArrayList<PluginState>();
/**
* HashMap that allows to access a {@link PluginState state} by the name
* of the plug-in
*/
private HashMap<String, PluginState> statesByName = new HashMap<String, PluginState>();
/**
* List of plugin heads. This is created before the plugin loading process starts.
* Access to this field should be synchronized!
*/
private ArrayList<PluginHead> pluginHeads = new ArrayList<PluginHead>();
/**
* Tells if the plugin loading process has already started.
*/
private boolean startedLoading = false;
/**
* Creates the shared instance.
* @param profileDir the profile directory
*/
public static void createInstance(File profileDir) {
if (sharedInstance == null) {
synchronized (PluginLoaderHi.class) {
if (sharedInstance == null)
sharedInstance = new PluginLoaderHi(profileDir);
}
}
}
/**
* Returns the shared instance of this loader.
* @return the shared instance of this loader or null (if it is not initialized yet).
*/
public static PluginLoaderHi getInstance() {
// This method is called from SvarogSecurityManager in privileged mode!
// NEVER give control to any plugin or untrusted code from here!
return sharedInstance;
}
/**
* Reads the {@link PluginDescription description} of the plug-in
* from an XML file.
* @param fileName the path to the XML file
* @return created description or null if there was an error while
* reading or parsing a file
*/
private PluginDescription readXml(String fileName) {
try {
return new PluginDescription(fileName);
} catch (Exception e) {
logger.warn("failed do read description of a plug-in from file "+fileName);
logger.error("", e);
return null;
}
}
/**
* Constructor. Sets the provided profile directory.
* @param profileDir the profile directory of the program.
*/
private PluginLoaderHi(File profileDir)
{
super();
this.pluginsStateFile = new File(profileDir + File.separator + "pluginsState.xml");
this.pluginsDirectoriesFile = new File(profileDir + File.separator + "plugin-locations.xml");
try {
if (!readPluginDirectories())
setDefaultPluginDir(profileDir);
setGlobalPluginDir();
readPluginsState(this.pluginsStateFile);
} catch (Exception e) {
final String errorMsg = "Failed to create loader of plug-ins";
logger.error(errorMsg, e);
}
}
final static FilenameFilter xml_file_filter =
new FilesystemFilter("xml", "Xml File", false);
/**
* Scans the given directory to find plug-ins.
* @param directory the directory to scan
*/
private void scanPluginDirectory(File directory) {
logger.debug("scanning over dir '" + directory + "'");
String[] filenames = directory.list(xml_file_filter);
for (String filename: filenames) {
logger.debug("looking at '" + filename + "'");
final PluginDescription descr = readXml(directory
+ File.separator + filename);
if (descr == null || !descr.fillURL(directory)) {
logger.warn("Skipping faulty plugin description: '" + descr + "'");
continue;
}
final String pluginName = descr.getName();
if (descriptionsByName.containsKey(pluginName)) {
PluginDescription first = descriptionsByName.get(pluginName);
logger.warn("Duplicate plugin: '" + first + "' and '" + descr + "'."
+ "Skipping the latter.");
continue;
}
descriptions.add(descr);
descriptionsByName.put(pluginName, descr);
final PluginState state = statesByName.get(descr.getName());
if (state != null && descr.isActive()) {
descr.setActive(state.isActive());
}
}
}
/**
* Adds plug-in directories for all default plug-ins, namely
* the directories {@code ../plugins/}*{@code /target}
* @param svarogDir the svarog base directory
*/
private void startFromSourcesAddPluginDirs(File svarogDir) {
File pluginsDir = new File(svarogDir + File.separator + ".." + File.separator + "plugins");
logger.info("trying to load plugins from '" + pluginsDir + "'");
if (pluginsDir.exists() && pluginsDir.canRead() && pluginsDir.isDirectory()) {
String[] pluginSrcDirsNames = pluginsDir.list();
for (String dirName : pluginSrcDirsNames) {
File dir = new File(pluginsDir, dirName);
if (dir.isDirectory()) {
File pluginDir = new File(dir + File.separator + "target");
if (pluginDir.exists() && pluginDir.isDirectory() && pluginDir.canRead()) {
globalPluginDirectories.add(pluginDir);
}
}
}
}
}
/**
* Sets the directory where the default plug-ins are stored.
* The location of the file depends on the fact if Svarog was started:
* <ul>
* <li>from sources,</li>
* <li>from jar file.</li>
* </ul>
*/
private void setGlobalPluginDir() {
//hack to get the location of the jar file and add the global plugin directory
URL srcURL = getClass().getProtectionDomain().getCodeSource().getLocation();
logger.debug("svarog is loaded from '" + srcURL + "'");
if (srcURL.toString().endsWith("/target/classes/")) {
File svarogDirFile = _urlToFile(srcURL);
svarogDirFile = svarogDirFile.getParentFile().getParentFile();
startFromSourcesAddPluginDirs(svarogDirFile);
} else {
final URLConnection connection;
try {
connection = srcURL.openConnection();
} catch (IOException ex) {
logger.error("failed to open connection to jar", ex);
return;
}
final File jarFile;
if (connection instanceof JarURLConnection) {
URL jarURL = ((JarURLConnection) connection).getJarFileURL();
jarFile = _urlToFile(jarURL);
} else {
// e.g. file:/usr/share/java/svarog-1.1.6.jar
jarFile = new File(srcURL.getPath());
}
File pluginsDir = new File(jarFile.getParentFile() + File.separator +
"svarog" + File.separator + "plugins");
if (pluginsDir.exists() && pluginsDir.isDirectory() && pluginsDir.canRead()) {
logger.info("trying to load plugins from '" + pluginsDir + "'");
globalPluginDirectories.add(pluginsDir);
return;
}
pluginsDir = new File(jarFile.getParentFile() + File.separator + "plugins");
if (pluginsDir.exists() && pluginsDir.isDirectory() && pluginsDir.canRead()) {
logger.info("trying to load plugins from '" + pluginsDir + "'");
globalPluginDirectories.add(pluginsDir);
return;
}
logger.warn("plugin dir not found");
}
}
private File _urlToFile(URL url) {
try {
return new File(url.toURI());
} catch (java.net.URISyntaxException ex) {
throw new RuntimeException(ex);
}
}
/**
* Adds the default plug-in directory based on given profile directory.
* Also:
* <ul>
* <li>adds plug-in directories for all default plug-ins if Svarog is
* started from sources,</li>
* <li>adds the global plug-in directory if Svarog is started from jar
* created during the installation,</li>
* </ul>
* @param profileDir profile directory where default plug-in folder
* is located
*/
private void setDefaultPluginDir(File profileDir) {
File pluginDir = new File(profileDir + File.separator + "plugins");
if (!pluginDir.exists())
pluginDir.mkdir();
if (pluginDir.exists() && pluginDir.isDirectory() && pluginDir.canRead()) {
this.pluginDirs.add(pluginDir);
}
}
/**
* Adds a "plugin options" button to the tools menu.
* To do it prepares a collection of plug-in states and uses it to
* create a dialog window which will be activated after clicking this button.
*/
private void addPluginOptions() {
ViewerElementManager manager = PluginAccessClass.getManager();
ArrayList<PluginState> existingPluginStates = new ArrayList<PluginState>();
for (PluginDescription descr : descriptions) {
PluginState pluginState = statesByName.get(descr.getName());
if (pluginState == null) {
pluginState = new PluginState(descr.getName(), descr.isActive());
states.add(pluginState);
statesByName.put(pluginState.getName(), pluginState);
}
existingPluginStates.add(pluginState);
pluginState.setMissingDependencies(descr.findMissingDependencies(descriptions));
pluginState.setVersion(descr.getVersion());
pluginState.setFailedToLoad(descr.isFailedToLoad());
}
PluginDialog pluginDialog = new PluginDialog(manager.getDialogParent(), true, existingPluginStates,
pluginDirs);
PluginAction action = new PluginAction(existingPluginStates);
action.setPluginDialog(pluginDialog);
manager.getToolsMenu().add(action);
}
/**
* Scans the all directories in {@link #pluginDirs} to find plug-ins.
*/
private void scanPluginDirectories() {
for (File plDir : pluginDirs) {
if (plDir.exists() && plDir.canRead() && plDir.isDirectory())
scanPluginDirectory(plDir);
}
for (File plDir : globalPluginDirectories) {
if (plDir.exists() && plDir.canRead() && plDir.isDirectory())
scanPluginDirectory(plDir);
}
boolean repeat = true;
while (repeat) {
repeat = false;
for (PluginDescription descr : descriptions) {
if (descr.isActive() && !descr.dependenciesSatisfied(descriptions))
repeat = true;
}
}
}
/**
* Creates a new ClassLoader and loads plug-ins using it.
* Invokes the {@link Plugin#register(org.signalml.plugin.export.SvarogAccess)}
* function of every plug-in to register the plug-in.
* Adds a {@code addPluginOptions()} button to tools menu.
*/
public void loadPlugins()
{
synchronized (this) {
this.startedLoading = true;
}
scanPluginDirectories();
ClassLoader prevCL = Thread.currentThread().getContextClassLoader();
sortActivePlugins();
createPluginHeads();
for (PluginHead head : pluginHeads) {
PluginDescription descr = head.getDescription();
if (! head.hasLoader()) {
head.setLoader(new PluginLoaderLo(head, prevCL));
}
if (! loadPlugin(head))
setDependentInactive(descr);
}
addPluginOptions();
PluginAccessClass.setInitializationPhaseEnd();
}
/**
* Try to load a plugin.
* @returns true iff success
*/
protected boolean loadPlugin(PluginHead head) {
final PluginDescription descr = head.getDescription();
final PluginLoaderLo loader = head.getLoader();
final Plugin plugin;
try {
logger.debug("Loading plugin " + descr.getName() +
" (class " + descr.getStartingClass() + ")");
plugin = (Plugin)(loader.loadClass(descr.getStartingClass())).newInstance();
} catch (Exception exc) {
String errorMsg = "Failed to load plugin " + descr.getName() +
" from " + descr.getJarFileURL();
logger.error(errorMsg, exc);
descr.setActive(false);
descr.setFailedToLoad(true);
return false;
}
head.setPluginObj(plugin);
try {
plugin.register(new PluginAccessClass(head));
} catch (Throwable exc) {
String errorMsg = "Failed to initialize plugin " + descr.getName() +
" from " + descr.getJarFileURL();
logger.error(errorMsg, exc);
}
return true;
}
/**
* Sets all plug-ins that are dependent from given to be
* inactive.
* @param description the description of the plugin
*/
private void setDependentInactive(PluginDescription description) {
for (PluginDescription descr : descriptions) {
if (descr.dependentFrom(description)) descr.setActive(false);
}
}
/**
* Reads the remembered state of the plug-in from given XML node.
* @param node the node to read state from
*/
private void readPluginState(Node node) {
NodeList nodeList = node.getChildNodes();
String name = "";
boolean active = false;
for (int i = 0; i < nodeList.getLength(); ++i) {
Node nodeTmp = nodeList.item(i);
if (nodeTmp.getNodeName().equals(XMLStatesPluginNameNode))
name = nodeTmp.getFirstChild().getNodeValue();
else if (nodeTmp.getNodeName().equals(XMLStatesPluginActiveNode))
active = Boolean.parseBoolean(nodeTmp.getFirstChild().getNodeValue());
}
PluginState state = new PluginState(name, active);
states.add(state);
statesByName.put(state.getName(), state);
}
/**
* Opens an XML file and returns the document element.
* @param file the file in which XML tree is stored
* @return the document element
* @throws ParserConfigurationException if an error occurs while
* creating a document builder
* @throws SAXException if parsing XML failed
* @throws IOException if I/O error occurs
*/
private Element openXMLDocument(File file) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder;
documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(file);
Element element = document.getDocumentElement();
element.normalize();
return element;
}
/**
* Reads the remembered state of all plug-ins from given
* XML file.
* @param fileName the path to the file
*/
private void readPluginsState(File fileName) {
try {
if (fileName.exists() && fileName.canRead()) {
Element element = openXMLDocument(fileName);
NodeList nodeList = element.getChildNodes();
for (int i = 0; i < nodeList.getLength(); ++i) {
Node node = nodeList.item(i);
if (node.getNodeName().equals(XMLStatesPluginNode))
readPluginState(node);
}
} else {
logger.debug("File with states of plugins doesn't exist. Default states are used");
}
} catch (Exception e) {
logger.error("Failed to load states of plug-ins from file. All plug-ins with unloaded states will be set inacitve.");
logger.error("", e);
}
}
/**
* Performs operations necessary while closing the program.
* Writes the desired state of plug-ins to an XML file.
*/
public void onClose() {
rememberPluginsState();
savePluginDirectories();
PluginAccessClass.onClose();
}
/**
* Creates a document used to save data in XML form
* @return created document
* @throws ParserConfigurationException if a DocumentBuilder cannot be created
*/
private Document createXMLDocumentToSave() throws ParserConfigurationException {
DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
Document doc = docBuilder.newDocument();
return doc;
}
/**
* Writes the provided data to XML file of a given name.
* @param path the path to the file
* @param data the XML document to be saved
* @throws FileNotFoundException If the given path does not denote
* an existing, writable regular file and a new regular file
* of that name cannot be created
* @throws TransformerException if transformation from
* DOMSource to StreamResult is not possible
*/
private void saveToXMLFile(File path, Document data) throws FileNotFoundException, TransformerException {
PrintStream ps = new PrintStream(path);
StreamResult result = new StreamResult(ps);
TransformerFactory transfac = TransformerFactory.newInstance();
Transformer trans = transfac.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
trans.setOutputProperty(OutputKeys.INDENT, "yes");
DOMSource source = new DOMSource(data);
trans.transform(source, result);
ps.close();
}
/**
* Writes the desired state of plug-ins to XML file.
* @return true if operation successful, false otherwise
*/
private boolean rememberPluginsState() {
try {
Document doc = createXMLDocumentToSave();
Element root = doc.createElement(XMLStatesPluginsNode);
doc.appendChild(root);
for (PluginState state : states) {
Element pluginNode = doc.createElement(XMLStatesPluginNode);
root.appendChild(pluginNode);
Element nameNode = doc.createElement(XMLStatesPluginNameNode);
nameNode.appendChild(doc.createTextNode(state.getName()));
pluginNode.appendChild(nameNode);
Element activeNode = doc.createElement(XMLStatesPluginActiveNode);
activeNode.appendChild(doc.createTextNode(state.isActive() ? "true" : "false"));
pluginNode.appendChild(activeNode);
}
saveToXMLFile(this.pluginsStateFile, doc);
return true;
} catch (Exception e) {
logger.error("failed to save states of plug-ins");
logger.error("", e);
return false;
}
}
/**
* Sorts plug-ins (actually their descriptions) by their dependencies
* (it is partial order).
* Plug-in {@code A} dependent from plug-in {@code B} will be always after
* plug-in {@code B}.
*/
private void sortActivePlugins() {
ArrayList<PluginDescription> toSort = new ArrayList<PluginDescription>(descriptions);
ArrayList<PluginDescription> sorted = new ArrayList<PluginDescription>();
while (!toSort.isEmpty()) {
for (PluginDescription descr : toSort) {
if (descr.notDependentFrom(toSort)) {
sorted.add(descr);
toSort.remove(descr);
break;
}
}
}
descriptions = sorted;
}
/**
* Populates {@link #pluginHeads} from {@link #descriptions}.
* Here we assume {@link #descriptions} are sorted!
*/
private void createPluginHeads() {
ArrayList<PluginHead> hl = new ArrayList<PluginHead>();
for (PluginDescription desc : this.descriptions) {
if (desc.dependenciesSatisfied(descriptions) && desc.isActive()) {
PluginHead head = new PluginHead(desc);
List<PluginDependency> depList = desc.getDependencies();
for (PluginDependency dep : depList) {
String depName = dep.getName();
PluginDescription depDesc = descriptionsByName.get(depName);
if (null != depDesc)
head.addDependency(depDesc.getHead());
}
desc.setHead(head);
hl.add(head);
}
}
synchronized (this) {
this.pluginHeads = hl;
}
}
/**
* Reads the names of directories in which plug-ins are stored
* from the XML configuration file.
* @return true if operation successful, false otherwise
*/
private boolean readPluginDirectories() {
try {
if (this.pluginsDirectoriesFile.exists()) {
Element element = openXMLDocument(this.pluginsDirectoriesFile);
NodeList nodeList = element.getChildNodes();
for (int i = 0; i < nodeList.getLength(); ++i) {
Node node = nodeList.item(i);
if (node.getNodeName().equals(XMLDirectoryNode)) {
File directoryToAdd = new File(node.getFirstChild().getNodeValue());
pluginDirs.add(directoryToAdd);
}
}
return true;
}
} catch (Exception e) {
logger.error("failed to read plug-in directories from file");
logger.error("", e);
}
return false;
}
/**
* Writes the names of directories in which plug-ins are stored
* to the XML configuration file.
*/
private void savePluginDirectories() {
try {
Document doc = createXMLDocumentToSave();
Element root = doc.createElement(XMLDirectoriesNode);
doc.appendChild(root);
for (File directory : pluginDirs) {
Element directoryNode = doc.createElement(XMLDirectoryNode);
root.appendChild(directoryNode);
directoryNode.appendChild(doc.createTextNode(directory.getPath()));
}
saveToXMLFile(this.pluginsDirectoriesFile, doc);
} catch (Exception e) {
logger.error("failed to save current plug-in directories");
logger.error("", e);
}
}
/**
* @return the pluginDirs
*/
public ArrayList<File> getPluginDirs() {
ArrayList<File> tmpPluginDirs = new ArrayList<File>(pluginDirs);
tmpPluginDirs.addAll(globalPluginDirectories);
return tmpPluginDirs;
}
/**
* Returns true iff the plugin loading process has already started.
* @return {@link #startedLoading}
*/
public synchronized boolean hasStartedLoading() {
return startedLoading;
}
public boolean hasLoaded(String className) {
if (! hasStartedLoading())
return false;
ArrayList<PluginHead> heads = new ArrayList<PluginHead>();
synchronized (this) {
heads.addAll(this.pluginHeads);
}
for (PluginHead h : heads) {
if (h.containsClass(className))
return true;
}
return false;
}
}