/* * The MIT License (MIT) * * Copyright (c) 2007-2015 Broad Institute * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.broad.igv.cli_plugin; import org.apache.log4j.Logger; import org.broad.igv.DirectoryManager; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.util.FileUtils; import org.broad.igv.util.HttpUtils; 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; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.*; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Class used to parse a cli_plugin specification file (currently XML). * <p/> * User: jacob * Date: 2012-Aug-03 */ public class PluginSpecReader { private static Logger log = Logger.getLogger(PluginSpecReader.class); protected String specPath; protected Document document; public static final String CUSTOM_PLUGINS_FILENAME = "custom_plugins.txt"; public static final String BUILTIN_PLUGINS_FILENAME = "builtin_plugins.txt"; public static final String COMMAND = "command"; public static final String CMD_ARG = "cmd_arg"; /** * List of plugins tha IGV knows about */ private static List<PluginSpecReader> pluginList; /** * List of tools described contained in this pluginSpec */ private List<Tool> tools; private PluginSpecReader(String path) { this.specPath = path; } /** * Create a new reader. Returns null if * the input path does not represent a valid cli_plugin spec, * or if there was any other problem parsing the document * * @param path * @return */ public static PluginSpecReader create(String path) { PluginSpecReader reader = new PluginSpecReader(path); if (!reader.parseDocument()) return null; return reader; } /** * True if the path exists and is executable, false if not (or null) * * @param execPath * @return */ public static boolean isToolPathValid(String execPath) { if (execPath == null) return false; execPath = FileUtils.findExecutableOnPath(execPath); File execFile = new File(execPath); boolean pathValid = execFile.isFile(); if (pathValid && !execFile.canExecute()) { log.warn(execPath + " exists but is not executable. "); } return pathValid; } public String getSpecPath() { return specPath; } public String getId() { return document.getDocumentElement().getAttribute("id"); } private boolean parseDocument() { boolean success = false; //We want to accept either a path within the JAR file (getResource), //or external path. Also we want to use builder.parse(String) so //that we can use relative links for DTD spec try { DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); URL url = getClass().getResource(specPath); String uri = null; if (url == null) { uri = FileUtils.getAbsolutePath(specPath, (new File(".")).getAbsolutePath()); } else { uri = url.toString(); } document = builder.parse(uri); success = document.getDocumentElement().getTagName().equals("cli_plugin"); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return success; } public List<Tool> getTools() { if (tools == null) { tools = PluginSpecReader.<Tool>unmarshalElementsByTag(document.getDocumentElement(), "tool"); } return tools; } private static <T> List<T> unmarshalElementsByTag(Element topElement, String tag) { NodeList nodes = topElement.getElementsByTagName(tag); List<T> outNodes = new ArrayList<T>(nodes.getLength()); for (int nn = 0; nn < nodes.getLength(); nn++) { outNodes.add(PluginSpecReader.<T>unmarshal(nodes.item(nn))); } return outNodes; } public static List<PluginSpecReader> getPlugins() { if (pluginList == null) { pluginList = generatePluginList(); } return pluginList; } /** * Return a list of cli_plugin specification files present on the users computer. * Checks the IGV installation directory (jar) as well as IGV data directory * * @return */ private static List<PluginSpecReader> generatePluginList() { List<PluginSpecReader> readers = new ArrayList<PluginSpecReader>(); //Guard against loading cli_plugin multiple times. May want to reconsider this, //and override equals of PluginSpecReader Set<String> pluginIds = new HashSet<String>(); try { File[] checkDirs = new File[]{ DirectoryManager.getIgvDirectory(), new File(FileUtils.getInstallDirectory()), new File(".") }; for (File checkDir : checkDirs) { File plugDir = new File(checkDir, "plugins"); File[] possPlugins = plugDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".xml"); } }); if (possPlugins == null) { possPlugins = new File[0]; } List<String> possPluginsList = new ArrayList<String>(possPlugins.length); for (File fi : possPlugins) { possPluginsList.add(fi.getAbsolutePath()); } //When a user adds a cli_plugin, we store the path here File customFile = new File(checkDir, CUSTOM_PLUGINS_FILENAME); if (customFile.canRead()) { BufferedReader br = new BufferedReader(new FileReader(customFile)); possPluginsList.addAll(getPluginPaths(br)); } //Builtin plugins. Do these last so custom ones take precedence for (String pluginName : getBuiltinPlugins()) { possPluginsList.add("resources/" + pluginName); } for (String possPlugin : possPluginsList) { PluginSpecReader reader = PluginSpecReader.create(possPlugin); if (reader != null && !pluginIds.contains(reader.getId())) { readers.add(reader); pluginIds.add(reader.getId()); } } } } catch (Exception e) { log.error(e); //Guess this user won't be able to use plugins } return readers; } static List<String> getBuiltinPlugins() throws IOException { InputStream contentsStream = PluginSpecReader.class.getResourceAsStream("resources/" + PluginSpecReader.BUILTIN_PLUGINS_FILENAME); BufferedReader inReader = new BufferedReader(new InputStreamReader(contentsStream)); return getPluginPaths(inReader); } private static List<String> getPluginPaths(BufferedReader reader) throws IOException { String line; List<String> pluginPaths = new ArrayList<String>(3); while ((line = reader.readLine()) != null) { if (line.startsWith("#")) { continue; } pluginPaths.add(line); } return pluginPaths; } /** * @param absolutePath Full path (can be URL) to cli_plugin */ public static void addCustomPlugin(String absolutePath) throws IOException { File outFile = new File(DirectoryManager.getIgvDirectory(), CUSTOM_PLUGINS_FILENAME); outFile.createNewFile(); BufferedWriter writer = new BufferedWriter(new FileWriter(outFile)); writer.write(absolutePath); writer.write("\n"); writer.flush(); writer.close(); pluginList = generatePluginList(); } public String getName() { return document.getDocumentElement().getAttribute("name"); } /** * Check the preferences for the tool path, using default from * XML spec if necessary * * @param tool * @return */ public String getToolPath(Tool tool) { //Check settings for path, use default if not there String toolPath = PreferencesManager.getPreferences().getToolPath(getId(), tool.name); if (toolPath == null) { toolPath = tool.defaultPath; } return toolPath; } private static JAXBContext jc = null; static JAXBContext getJAXBContext() throws JAXBException { if (jc == null) { jc = JAXBContext.newInstance(Tool.class, Command.class, Argument.class, Parser.class); } return jc; } static <T> T unmarshal(Node node) { try { Unmarshaller u = getJAXBContext().createUnmarshaller(); u.setListener(ToolListener.getInstance()); //TODO change schema to W3C //u.setSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(myFile); return (T) u.unmarshal(node); } catch (JAXBException e) { throw new RuntimeException(e); } } /** * Each tool may contain default settings for each constituent command, we write * those settings into each command after unmarshalling */ private static class ToolListener extends Unmarshaller.Listener { private static ToolListener instance; @Override public void afterUnmarshal(Object target, Object parent) { super.afterUnmarshal(target, parent); if (target instanceof Tool) { Tool tool = (Tool) target; /** * We rewrite the arguments/outputs from the defaults, if * defaults are available and nothing is overriding them. * Note that defaultArgs and defaultOutputs should be independent of each other */ boolean hasDefaultArgs = tool.defaultArgs != null; boolean hasDefaultOutputs = tool.defaultOutputs != null; if (!hasDefaultArgs && !hasDefaultOutputs) return; for (Command command : ((Tool) target).commandList) { if (hasDefaultArgs && command.argumentList == null) command.argumentList = tool.defaultArgs.argumentList; if (hasDefaultOutputs && command.outputList == null) command.outputList = tool.defaultOutputs.outputList; } } } public static Unmarshaller.Listener getInstance() { if (instance == null) instance = new ToolListener(); return instance; } } /** * Represents an individual command line tool/executable, e.g. bedtools */ @XmlRootElement @XmlAccessorType(XmlAccessType.NONE) public static class Tool { @XmlAttribute public String name; @XmlAttribute public String defaultPath; @XmlAttribute public boolean visible; @XmlAttribute public String toolUrl; @XmlAttribute public String helpUrl; @XmlAttribute public boolean forbidEmptyOutput = false; /** * Contains the default settings for input arguments */ @XmlElement(name = "default_arg") private Command defaultArgs; /** * Contains the default settings for parsing output */ @XmlElement(name = "default_output") private Command defaultOutputs; @XmlElement(name = "msg") public List<String> msgList; @XmlElement(name = "command") public List<Command> commandList; } /** * Description of output returned from tool * User: jacob * Date: 2012-Dec-27 */ @XmlAccessorType(XmlAccessType.NONE) public static class Output { @XmlAttribute public String name; @XmlAttribute public String defaultValue; @XmlAttribute public OutputType type = OutputType.FEATURE_TRACK; @XmlElement public Parser parser; } /** * Type of data returned from tool */ @XmlEnum @XmlAccessorType(XmlAccessType.NONE) public static enum OutputType { @XmlEnumValue("FeatureTrack") FEATURE_TRACK, @XmlEnumValue("DataSourceTrack") DATA_SOURCE_TRACK, @XmlEnumValue("VariantTrack") VARIANT_TRACK } /** * Description of how to parse each line read back from command line tool * User: jacob * Date: 2012-Dec-27 */ @XmlAccessorType(XmlAccessType.NONE) public static class Parser { public static String SOURCE_STDOUT = "stdout"; @XmlAttribute boolean strict; @XmlAttribute String format; @XmlAttribute String decodingCodec; @XmlAttribute String source = SOURCE_STDOUT; @XmlElement String[] libs; } /** * Represents a single command to be applied to a tool. e.g. intersect */ @XmlAccessorType(XmlAccessType.NONE) public static class Command { @XmlAttribute public String name; @XmlAttribute public String cmd = ""; @XmlElement(name = "arg") public List<Argument> argumentList; @XmlElement(name = "output") public List<Output> outputList; } public static URL[] getLibURLs(String[] libPaths, String absRoot) throws MalformedURLException { if (libPaths == null) return null; List<URL> urls = new ArrayList<URL>(libPaths.length); for (String libPath : libPaths) { String urlPath = libPath; if (HttpUtils.isRemoteURL(urlPath) || urlPath.startsWith("file://")) { //do nothing } else if ((new File(urlPath)).isAbsolute()) { urlPath = "file://" + urlPath; } else { //Relative path urlPath = "file://" + absRoot + "/" + urlPath; } urls.add(new URL(urlPath)); } return urls.toArray(new URL[0]); } }