/**
*
*/
package org.signalml.plugin.loader;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.cli.ParseException;
import org.apache.log4j.Logger;
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;
/**
* This class describes the plug-in. Extends the {@link PluginState state} of
* the plug-in.
* Apart from parameters of the state, contains:
* <ul>
* <li>the string with the full name of the class,
* that will be loaded to register the plug-in,</li>
* <li>the name of the jar file with the plug-in,</li>
* <li>the package that is exported by the plug-in,</li>
* <li>the list of {@link PluginDependency dependencies} of the plug-in.</li>
* </ul>
* The parameters of this description are read from an XML file.
* This description allows to:
* <ul>
* <li>check if its dependencies are satisfied,</li>
* <li>find missing dependencies.</li>
* </ul>
* @author Marcin Szumski
*/
public class PluginDescription extends PluginState {
private static final Logger logger = Logger.getLogger(PluginDescription.class);
/**
* File this description has been loaded from.
*/
private File descriptionFile;
/**
* the string with the full name of the class,
* that will be loaded to register the plug-in
*/
private String startingClass = null;
/**
* the name of the jar file with the plug-in
*/
private String jarFile = null;
/**
* {@link #jarFile} URL.
*/
private URL jarFileURL;
/**
* the name of the package that is exported by the plug-in
*/
private String exportPackage;
/**
* the list of {@link PluginDependency dependencies}
* of the plug-in
*/
private ArrayList<PluginDependency> dependencies = new ArrayList<PluginDependency>();
/**
* Plugin descriptor. This helps with dependency management during loading.
*/
private PluginHead pluginHead;
/**
* Functions which checks if a variable is null and (if it is) adds its
* name to the string.
* @param missingValues the string enlisting missing values
* @param variable the variable to be checked
* @param variableName the name of the variable
*/
private String addMissing(String missingValues, Object variable, String variableName) {
if (variable == null) {
if (missingValues.length() != 0)
missingValues += ", ";
missingValues += variableName;
}
return missingValues;
}
/**
* Constructor. Parses the XML file of a given path, which
* contains the description of the plug-in.
* @param fileName the path to an XML file with the
* description of the plug-in
* @throws ParserConfigurationException if a DocumentBuilder
* cannot be created
* @throws SAXException if the creation of document builder fails
* @throws IOException if an error while parsing the file occurs
* @throws ParseException if the xml file doesn't contain all necessary
* values
*/
public PluginDescription(String fileName) throws ParserConfigurationException,
SAXException, IOException, ParseException {
logger.info("loading description from " + fileName);
File descFile = new File(fileName);
setDescriptionFile(descFile);
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(descFile);
Element element = document.getDocumentElement();
element.normalize();
NodeList nodeList = element.getChildNodes();
for (int i = 0; i < nodeList.getLength(); ++i) {
Node node = nodeList.item(i);
if (node.getNodeName().equals("name"))
if (name == null)
name = node.getFirstChild().getNodeValue().trim();
else
logger.warn("duplicate plugin name node in xml file: " + fileName + " Used only first node.");
else if (node.getNodeName().equals("jar-file"))
if (jarFile == null)
jarFile = node.getFirstChild().getNodeValue().trim();
else
logger.warn("duplicate plugin jar-file node in xml file: " + fileName + " Used only first node.");
else if (node.getNodeName().equals("version"))
if (version == null)
setVersion(node.getFirstChild().getNodeValue().trim());
else
logger.warn("duplicate plugin version node in xml file: " + fileName + " Used only first node.");
else if (node.getNodeName().equals("starting-class"))
if (startingClass == null)
startingClass = node.getFirstChild().getNodeValue().trim();
else
logger.warn("duplicate plugin starting-class node in xml file: " + fileName + " Used only first node.");
else if (node.getNodeName().equals("export-package"))
exportPackage =node.getFirstChild().getNodeValue().trim();
else if (node.getNodeName().equals("dependencies"))
parseDependencies(node);
}
String missingValues = new String();
missingValues = addMissing(missingValues, name, "name");
missingValues = addMissing(missingValues, version, "version");
missingValues = addMissing(missingValues, jarFile, "jar-file");
missingValues = addMissing(missingValues, startingClass, "starting-class");
if (missingValues.length() > 0)
throw new ParseException("the xml file (" + fileName + ") doesn't contain all necessary values. Missing values: " + missingValues + ".");
}
/**
* Parses the subtree with dependencies starting from the given node.
* @param node XML node containing the dependencies of the plug-in
*/
private void parseDependencies(Node node) {
NodeList nodeList = node.getChildNodes();
for (int i = 0; i < nodeList.getLength(); ++i) {
Node dependencyNode = nodeList.item(i);
if (dependencyNode.getNodeName().equals("dependency")) {
NodeList dependencyNodeList = dependencyNode.getChildNodes();
String name = null;
String minimumVersion = null;
for (int j = 0; j < dependencyNodeList.getLength(); ++j) {
Node nodeTmp = dependencyNodeList.item(j);
if (nodeTmp.getNodeName().equals("name"))
name = nodeTmp.getFirstChild().getNodeValue().trim();
else if (nodeTmp.getNodeName().equals("version"))
minimumVersion = nodeTmp.getFirstChild().getNodeValue().trim();
}
if (name != null && minimumVersion != null) {
PluginDependency dependency = new PluginDependency(name, minimumVersion);
dependencies.add(dependency);
}
}
}
}
/**
* @return the full name of the starting class (class loaded to register this plug-in)
*/
public String getStartingClass() {
return startingClass;
}
/**
* @return the name of the jar file with this plug-in
*/
public String getJarFile() {
return jarFile;
}
/**
* @return the name of the package exported by this plug-in
*/
public String getExportPackage() {
return exportPackage;
}
/**
* Tells if all dependencies of the described plug-in are
* satisfied by any of the plug-ins on the list
* @param descriptions the list of all descriptions of plug-ins
* @return true if all dependencies satisfied, false otherwise
*/
public boolean dependenciesSatisfied(ArrayList<PluginDescription> descriptions) {
for (PluginDependency dep : dependencies) {
if (!dep.satisfied(descriptions))
{
setActive(false);
return false;
}
}
return true;
}
/**
* Creates an arrayList containing the dependencies that are not
* satisfied.
* @param descriptions the list of all descriptions of plug-ins
* @return the created array
*/
public ArrayList<PluginDependency> findMissingDependencies(ArrayList<PluginDescription> descriptions) {
ArrayList<PluginDependency> missingDependencies = new ArrayList<PluginDependency>();
for (PluginDependency dep: dependencies) {
if (!dep.satisfied(descriptions)) {
missingDependencies.add(dep);
}
}
return missingDependencies;
}
@Override
public String toString() {
// TODO where is this used? can you modify this string
// to include description file name?
return name.concat(" v").concat(versionToString());
}
/**
* Returns true if this plug-in is not dependent from any plug-in from the
* list.
* @param descriptions the list of plug-ins
* @return true if this plug-in is not dependent from any plug-in from the
* list, false otherwise
*/
public boolean notDependentFrom(ArrayList<PluginDescription> descriptions) {
for (PluginDescription descr : descriptions) {
if (dependentFrom(descr)) return false;
}
return true;
}
/**
* Returns true if this plug-in depends on the given plug-in.
* @param description the description of the plug-in
* @return true if this plug-in depends on the given plug-in,
* false otherwise
*/
public boolean dependentFrom(PluginDescription description) {
for (PluginDependency dependency : dependencies) {
if (dependency.getName().equals(description.getName())) return true;
}
return false;
}
private void setDescriptionFile(File f) {
this.descriptionFile = f;
}
protected void setJarFileURL(URL u) {
this.jarFileURL = u;
}
/**
* Returns the file this description has been loaded from. May be null.
* @return file this description has been loaded from (null if none)
*/
public File getDescriptionFile() {
return descriptionFile;
}
/**
* Returns the URL of this plugin JAR file.
* @return URL of this plugin JAR file (or null)
*/
public URL getJarFileURL() {
return jarFileURL;
}
/**
* Set URL based on plugin description and given directory.
* @return true if JAR file is found and can be read
*/
public boolean fillURL(File directory) {
final String name = directory.toURI().toString().concat(this.getJarFile());
final URL url;
try {
url = new URL(name);
} catch (MalformedURLException e) {
logger.error("failed to create URL for file "+name);
logger.error("", e);
return false;
}
File file = new File(directory, this.getJarFile());
if (!file.exists()) {
logger.error("File '" + file.getAbsolutePath() +
"' does not exist");
return false;
} else if (!file.canRead()) {
logger.error("File '" + file.getAbsolutePath() +
"' cannot be read.");
return false;
} else {
this.setJarFileURL(url);
logger.info(this.toString() + " will use " + url);
return true;
}
}
protected List<PluginDependency> getDependencies() {
return Collections.unmodifiableList(dependencies);
}
protected PluginHead getHead() {
return pluginHead;
}
protected void setHead(PluginHead h) {
this.pluginHead = h;
}
}