/* * File XMLParser.java * * Copyright (C) 2010 Remco Bouckaert remco@cs.auckland.ac.nz * * This file is part of BEAST2. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership and licensing. * * BEAST is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * BEAST is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with BEAST; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ package beast.util; import static beast.util.XMLParserUtils.processPlates; import static beast.util.XMLParserUtils.replaceVariable; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.StringReader; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import beast.app.beauti.PartitionContext; import beast.core.BEASTInterface; import beast.core.Distribution; import beast.core.Input; import beast.core.Input.Validate; import beast.core.Logger; import beast.core.Operator; import beast.core.Param; import beast.core.Runnable; import beast.core.State; import beast.core.parameter.Map; import beast.core.parameter.Parameter; import beast.core.parameter.RealParameter; import beast.core.util.Log; import beast.evolution.alignment.Alignment; import beast.evolution.alignment.Sequence; import beast.evolution.tree.Tree; /** * XMLParser process Beast 2 XML and constructs an MCMC object * <p/> * <p/> * Reserved elements (present in element2class map) * <distribution> * <operator> * <logger> * <data> * <sequence> * <state> * <parameter> * <tree> * <beast version='2.0' namespace='x.y.z:'> * <map name='elementName'>x.y.z.Class</map> * <run> * <plate> * <input> * <p/> * Reserved attributes: * <input id='myId' idRef='otherId' name='inputName' spec='x.y.z.MyClass'/> * <p/> * Reserved attribute formats: * shortcut for idref inputs * <input xyz='@ref'/> * == * <input> * <input name='xyz' idref='ref'/> * </input> * <p/> * plate notations * <plate var='n' range='1,2,3'><xyz id='id$(n)'/></plate> * == * <xyz id='id1'/> * <xyz id='id2'/> * <xyz id='id3'/> * <p/> * Resolving class: * 1. specified in spec attribute * 2. if not, get from element2class map * 3. if not, use element name (and hope it shows up in the namespace somewhere). * <p/> * Resolving name: * 1. specified in name attribute * 2. if not, use (non-reserved) attribute name * 3. if not, use element name * 4. if input, use 'value' when there is text content, but no element content * <p/> * Resolving value: * 0. if idref is specified, use the referred object * 1. specified in value attribute * 2. if not, use value of (non-reserved) attribute * 3. if not, use text content when there is text content, but no element content * <p/> * Parsing rules: * <p/> * Processing non reserved attributes * <input otherAttribute="xyz"/> * equals * <input> * <input name='otherAttribute' value='xyz'/> * </input> * <p/> * Processing non reserved element names * <myElement/> * == * <input spec='myElement' name='myElement'/> * unless 'spec' is a specified attribute, then that overrides, likewise for 'name' * <p/> * Processing of text content (only when there are no enclosing elements) * <input name='data'>xyz</input> * == * <input name='data' value='xyz/> * * @author rrb */ public class XMLParser { final static String DATA_CLASS = Alignment.class.getName(); final static String SEQUENCE_CLASS = Sequence.class.getName(); final static String STATE_CLASS = State.class.getName(); final static String LIKELIHOOD_CLASS = Distribution.class.getName(); final static String LOG_CLASS = Logger.class.getName(); final static String OPERATOR_CLASS = Operator.class.getName(); final static String REAL_PARAMETER_CLASS = RealParameter.class.getName(); final static String BEAST_INTERFACE_CLASS = BEASTInterface.class.getName(); final static String INPUT_CLASS = Input.class.getName(); final static String TREE_CLASS = Tree.class.getName(); final static String RUNNABLE_CLASS = Runnable.class.getName(); /* This is the set of keywords in XML. * This list should not be added to unless there * is a very very good reason. */ public final static String BEAST_ELEMENT = "beast"; public final static String MAP_ELEMENT = "map"; public final static String DISTRIBUTION_ELEMENT = "distribution"; public final static String OPERATOR_ELEMENT = "operator"; public final static String INPUT_ELEMENT = "input"; public final static String LOG_ELEMENT = "logger"; public final static String DATA_ELEMENT = "data"; public final static String SEQUENCE_ELEMENT = "sequence"; public final static String STATE_ELEMENT = "state"; public final static String TREE_ELEMENT = "tree"; public final static String REAL_PARAMETER_ELEMENT = "parameter"; public final static String RUN_ELEMENT = "run"; public final static String PLATE_ELEMENT = "plate"; Runnable m_runnable; State m_state; /** * DOM document representation of XML file * */ Document doc; /** * maps sequence data onto integer value * */ String m_sDataMap; HashMap<String, BEASTInterface> IDMap; HashMap<String, Integer[]> likelihoodMap; HashMap<String, Node> IDNodeMap; String unavailablePacakges = ""; static HashMap<String, String> element2ClassMap; static Set<String> reservedElements; static { element2ClassMap = new HashMap<>(); element2ClassMap.put(DISTRIBUTION_ELEMENT, LIKELIHOOD_CLASS); element2ClassMap.put(OPERATOR_ELEMENT, OPERATOR_CLASS); element2ClassMap.put(INPUT_ELEMENT, INPUT_CLASS); element2ClassMap.put(LOG_ELEMENT, LOG_CLASS); element2ClassMap.put(DATA_ELEMENT, DATA_CLASS); element2ClassMap.put(STATE_ELEMENT, STATE_CLASS); element2ClassMap.put(SEQUENCE_ELEMENT, SEQUENCE_CLASS); element2ClassMap.put(TREE_ELEMENT, TREE_CLASS); element2ClassMap.put(REAL_PARAMETER_ELEMENT, REAL_PARAMETER_CLASS); reservedElements = new HashSet<>(); for (final String element : element2ClassMap.keySet()) { reservedElements.add(element); } } public static class NameValuePair { String name; Object value; boolean processed; public NameValuePair(String name, Object value) { this.name = name; this.value = value; processed = false; } } List<BEASTInterface> beastObjectsWaitingToInit; List<Node> nodesWaitingToInit; public HashMap<String, String> getElement2ClassMap() { return element2ClassMap; } String[] nameSpaces; /** * Flag to indicate initAndValidate should be called after * all inputs of a beast object have been parsed */ boolean needsInitialisation = true; /** * when parsing XML, missing inputs can be assigned default values through * a RequiredInputProvider */ RequiredInputProvider requiredInputProvider = null; PartitionContext partitionContext = null; public XMLParser() { beastObjectsWaitingToInit = new ArrayList<>(); nodesWaitingToInit = new ArrayList<>(); } public Runnable parseFile(final File file) throws SAXException, IOException, ParserConfigurationException, XMLParserException { // parse the XML file into a DOM document final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); //factory.setValidating(true); doc = factory.newDocumentBuilder().parse(file); doc.normalize(); processPlates(doc,PLATE_ELEMENT); // Substitute occurrences of "$(filebase)" with name of file int pointIdx = file.getName().lastIndexOf('.'); String baseName = pointIdx<0 ? file.getName() : file.getName().substring(0, pointIdx); if (doc.getElementsByTagName(BEAST_ELEMENT).item(0) == null) { Log.err.println("Incorrect XML: Could not find 'beast' element in file " + file.getName()); throw new RuntimeException(); } replaceVariable(doc.getElementsByTagName(BEAST_ELEMENT).item(0), "filebase", baseName); // Substitute occurrences of "$(seed)" with RNG seed replaceVariable(doc.getElementsByTagName(BEAST_ELEMENT).item(0), "seed", String.valueOf(Randomizer.getSeed())); IDMap = new HashMap<>(); likelihoodMap = new HashMap<>(); IDNodeMap = new HashMap<>(); parse(); //assert m_runnable == null || m_runnable instanceof Runnable; if (m_runnable != null) return m_runnable; else { throw new XMLParserException("Run element does not point to a runnable object."); } } // parseFile /** * extract all elements (runnable or not) from an XML fragment. * Useful for retrieving all non-runnable elements when a template * is instantiated by Beauti * @throws ParserConfigurationException * @throws IOException * @throws SAXException * */ public List<BEASTInterface> parseTemplate(final String xml, final HashMap<String, BEASTInterface> idMap, final boolean initialise) throws XMLParserException, SAXException, IOException, ParserConfigurationException { needsInitialisation = initialise; // parse the XML file into a DOM document final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); //factory.setValidating(true); doc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); doc.normalize(); processPlates(doc,PLATE_ELEMENT); //XMLParserUtils.saveDocAsXML(doc, "/tmp/beast2.xml"); IDMap = idMap;//new HashMap<>(); likelihoodMap = new HashMap<>(); IDNodeMap = new HashMap<>(); final List<BEASTInterface> beastObjects = new ArrayList<>(); // find top level beast element final NodeList nodes = doc.getElementsByTagName("*"); if (nodes == null || nodes.getLength() == 0) { throw new XMLParserException("Expected top level beast element in XML"); } final Node topNode = nodes.item(0); // sanity check that we are reading a beast 2 file final double version = getAttributeAsDouble(topNode, "version"); if (!topNode.getNodeName().equals(BEAST_ELEMENT) || version < 2.0 || version == Double.MAX_VALUE) { return beastObjects; } // only process templates // String typeName = getAttribute(topNode, "type"); // if (typeName == null || !typeName.equals("template")) { // return beastObjects; // } // sanity check that required packages are installed initIDNodeMap(topNode); parseNameSpaceAndMap(topNode); final NodeList children = topNode.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { final Node child = children.item(i); Log.warning.println(child.getNodeName()); if (!child.getNodeName().equals(MAP_ELEMENT)) { beastObjects.add(createObject(child, BEAST_INTERFACE_CLASS)); } } } initBEASTObjects(); return beastObjects; } // parseTemplate private void initBEASTObjects() throws XMLParserException { Node node = null; try { for (int i = 0; i < beastObjectsWaitingToInit.size(); i++) { final BEASTInterface beastObject = beastObjectsWaitingToInit.get(i); node = nodesWaitingToInit.get(i); beastObject.initAndValidate(); } } catch (Exception e) { // next lines for debugging only //beastObject.validateInputs(); //beastObject.initAndValidate(); e.printStackTrace(); throw new XMLParserException(node, "validate and intialize error: " + e.getMessage(), 110); } } /** * Parse an XML fragment representing a Plug-in * Only the run element or if that does not exist the last child element of * the top level <beast> element is considered. * @throws XMLParserException */ public BEASTInterface parseFragment(final String xml, final boolean initialise) throws XMLParserException { needsInitialisation = initialise; // parse the XML fragment into a DOM document final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { doc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); } catch (SAXException | IOException | ParserConfigurationException e) { throw new RuntimeException(e); } doc.normalize(); processPlates(doc,PLATE_ELEMENT); IDMap = new HashMap<>(); likelihoodMap = new HashMap<>(); IDNodeMap = new HashMap<>(); // find top level beast element final NodeList nodes = doc.getElementsByTagName("*"); if (nodes == null || nodes.getLength() == 0) { throw new XMLParserException("Expected top level beast element in XML"); } final Node topNode = nodes.item(0); initIDNodeMap(topNode); parseNameSpaceAndMap(topNode); final NodeList children = topNode.getChildNodes(); if (children.getLength() == 0) { throw new XMLParserException("Need at least one child element"); } int i = children.getLength() - 1; while (i >= 0 && (children.item(i).getNodeType() != Node.ELEMENT_NODE || !children.item(i).getNodeName().equals("run"))) { i--; } if (i < 0) { i = children.getLength() - 1; while (i >= 0 && children.item(i).getNodeType() != Node.ELEMENT_NODE) { i--; } } if (i < 0) { throw new XMLParserException("Need at least one child element"); } final BEASTInterface beastObject = createObject(children.item(i), BEAST_INTERFACE_CLASS); initBEASTObjects(); return beastObject; } // parseFragment /** * Parse XML fragment that will be wrapped in a beast element * before parsing. This allows for ease of creating beast objects, * like this: * Tree tree = (Tree) new XMLParser().parseBareFragment("<tree spec='beast.util.TreeParser' newick='((1:1,3:1):1,2:2)'/>"); * to create a simple tree. */ public BEASTInterface parseBareFragment(String xml, final boolean initialise) throws XMLParserException { // get rid of XML processing instruction xml = xml.replaceAll("<\\?xml[^>]*>", ""); if (xml.contains("<beast")) { return parseFragment(xml, initialise); } else { return parseFragment("<beast>" + xml + "</beast>", initialise); } } public List<BEASTInterface> parseBareFragments(final String xml, final boolean initialise) throws XMLParserException, SAXException, IOException, ParserConfigurationException { needsInitialisation = initialise; // parse the XML fragment into a DOM document final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); doc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); doc.normalize(); processPlates(doc,PLATE_ELEMENT); // find top level beast element final NodeList nodes = doc.getElementsByTagName("*"); if (nodes == null || nodes.getLength() == 0) { throw new XMLParserException("Expected top level beast element in XML"); } final Node topNode = nodes.item(0); initIDNodeMap(topNode); parseNameSpaceAndMap(topNode); final NodeList children = topNode.getChildNodes(); final List<BEASTInterface> beastObjects = new ArrayList<>(); for (int i = 0; i < children.getLength(); i++) { if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { final BEASTInterface beastObject = createObject(children.item(i), BEAST_INTERFACE_CLASS); beastObjects.add(beastObject); } } initBEASTObjects(); return beastObjects; } /** * parse BEAST file as DOM document * @throws XMLParserException */ public void parse() throws XMLParserException { // find top level beast element final NodeList nodes = doc.getElementsByTagName("*"); if (nodes == null || nodes.getLength() == 0) { throw new XMLParserException("Expected top level beast element in XML"); } final Node topNode = nodes.item(0); final double version = getAttributeAsDouble(topNode, "version"); if (version < 2.0 || version == Double.MAX_VALUE) { throw new XMLParserException(topNode, "Wrong version: only versions > 2.0 are supported", 101); } String required = getAttribute(topNode, "required"); if (required != null && required.trim().length() > 0) { String [] packageAndVersions = required.split(":"); for (String s : packageAndVersions) { s = s.trim(); int i = s.lastIndexOf(" "); if (i > 0) { String pkgname = s.substring(0, i); String pkgversion = s.substring(i+1); if (!AddOnManager.isInstalled(pkgname, pkgversion)) { unavailablePacakges += s +", "; } } } if (unavailablePacakges.length() > 1) { unavailablePacakges = unavailablePacakges.substring(0, unavailablePacakges.length() - 2); if (unavailablePacakges.contains(",")) { Log.warning("The following packages are required, but not available: " + unavailablePacakges); } else { Log.warning("The following package is required, but is not available: " + unavailablePacakges); } Log.warning("See http://beast2.org/managing-packages/ for details on how to install packages."); } } initIDNodeMap(topNode); parseNameSpaceAndMap(topNode); //parseState(); parseRunElement(topNode); initBEASTObjects(); } // parse /** * Traverse DOM beast.tree and grab all nodes that have an 'id' attribute * Throw exception when a duplicate id is encountered * * @param node * @throws XMLParserException */ void initIDNodeMap(final Node node) throws XMLParserException { final String id = getID(node); if (id != null) { if (IDNodeMap.containsKey(id)) { throw new XMLParserException(node, "IDs should be unique. Duplicate id '" + id + "' found", 104); } IDNodeMap.put(id, node); } final NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { initIDNodeMap(children.item(i)); } } /** * find out namespaces (beast/@namespace attribute) * and element to class maps, which reside in beast/map elements * <beast version='2.0' namespace='snap:beast.util'> * <map name='snapprior'>snap.likelihood.SnAPPrior</map> * <map name='snaplikelihood'>snap.likelihood.SnAPTreeLikelihood</map> * * @param topNode * @throws XMLParserException */ void parseNameSpaceAndMap(final Node topNode) throws XMLParserException { // process namespaces if (hasAtt(topNode, "namespace")) { final String nameSpace = getAttribute(topNode, "namespace"); setNameSpace(nameSpace); } else { // make sure that the default namespace is in there if (this.nameSpaces == null) { this.nameSpaces = new String[1]; this.nameSpaces[0] = ""; } } // process map elements final NodeList nodes = doc.getElementsByTagName(MAP_ELEMENT); for (int i = 0; i < nodes.getLength(); i++) { final Node child = nodes.item(i); final String name = getAttribute(child, "name"); if (name == null) { throw new XMLParserException(child, "name attribute expected in map element", 300); } if (!element2ClassMap.containsKey(name)) { // throw new XMLParserException(child, "name '" + name + "' is already defined as " + m_sElement2ClassMap.get(name), 301); // } // get class String clazz = child.getTextContent(); // remove spaces clazz = clazz.replaceAll("\\s", ""); // go through namespaces in order they are declared to find the correct class boolean isDone = false; for (final String nameSpace : this.nameSpaces) { try { // sanity check: class should exist if (!isDone && Class.forName(nameSpace + clazz) != null) { element2ClassMap.put(name, clazz); Log.debug.println(name + " => " + nameSpace + clazz); final String reserved = getAttribute(child, "reserved"); if (reserved != null && reserved.toLowerCase().equals("true")) { reservedElements.add(name); } isDone = true; } } catch (ClassNotFoundException e) { // Log.warning.println("Not found " + e.getMessage()); // TODO: handle exception } } } } } // parseNameSpaceAndMap public void setNameSpace(final String nameSpaceStr) { final String[] nameSpaces = nameSpaceStr.split(":"); // append dot after every non-zero namespace this.nameSpaces = new String[nameSpaces.length + 1]; int i = 0; for (String nameSpace : nameSpaces) { nameSpace = nameSpace.trim(); if (nameSpace.length() > 0) { if (nameSpace.charAt(nameSpace.length() - 1) != '.') { nameSpace += '.'; } } this.nameSpaces[i++] = nameSpace; } // make sure that the default namespace is in there this.nameSpaces[i] = ""; } void parseRunElement(final Node topNode) throws XMLParserException { // find mcmc element final NodeList nodes = doc.getElementsByTagName(RUN_ELEMENT); if (nodes.getLength() == 0) { throw new XMLParserException(topNode, "Expected run element in file", 102); } if (nodes.getLength() > 1) { throw new XMLParserException(topNode, "Expected only one mcmc element in file, not " + nodes.getLength(), 103); } final Node mcmc = nodes.item(0); m_runnable = (Runnable) createObject(mcmc, RUNNABLE_CLASS); } // parseMCMC /** * Check that beast object is a class that is assignable to class with name className. * This involves a parameter clutch to deal with non-real parameters. * This needs a bit of work, obviously... * @throws ClassNotFoundException */ boolean checkType(final String className, final BEASTInterface beastObject, Node node) throws XMLParserException { try { if (className.equals(INPUT_CLASS) || Class.forName(className).isInstance(beastObject)) { return true; } } catch (ClassNotFoundException e) { throw new XMLParserException(node, "Class not found:" + e.getMessage(), 444); } // parameter clutch if (className.equals(RealParameter.class.getName()) && beastObject instanceof Parameter<?>) { return true; } return false; } // checkType BEASTInterface createObject(final Node node, final String classname) throws XMLParserException { // try the IDMap first final String id = getID(node); if (id != null) { if (IDMap.containsKey(id)) { final BEASTInterface beastObject = IDMap.get(id); if (checkType(classname, beastObject, node)) { return beastObject; } throw new XMLParserException(node, "id=" + id + ". Expected object of type " + classname + " instead of " + beastObject.getClass().getName(), 105); } } final String dRef = getIDRef(node); if (dRef != null) { // produce warning if there are other attributes than idref if (node.getAttributes().getLength() > 1) { // check if there are just 2 attributes and other attribute is 'name' and/or 'id' final int offset = (getAttribute(node, "id") == null? 0: 1) + (getAttribute(node, "name") == null? 0: 1); if (node.getAttributes().getLength() > 1 + offset) { Log.warning.println("Element " + node.getNodeName() + " found with idref='" + dRef + "'. All other attributes are ignored.\n"); } } if (IDMap.containsKey(dRef)) { final BEASTInterface beastObject = IDMap.get(dRef); // TODO: testing for "Alignment" is a hack for a common problem // occurring in many templates. As long as packages are not debugged // this hack should stay in place, but should eventually (v2.5?) be removed. if (classname.equals("Alignment") || checkType(classname, beastObject, node)) { return beastObject; } checkType(classname, beastObject, node); throw new XMLParserException(node, "id=" + dRef + ". Expected object of type " + classname + " instead of " + beastObject.getClass().getName(), 106); } else if (IDNodeMap.containsKey(dRef)) { final BEASTInterface beastObject = createObject(IDNodeMap.get(dRef), classname); if (checkType(classname, beastObject, node)) { return beastObject; } throw new XMLParserException(node, "id=" + dRef + ". Expected object of type " + classname + " instead of " + beastObject.getClass().getName(), 107); } throw new XMLParserException(node, "Could not find object associated with idref " + dRef, 170); } // it's not in the ID map yet, so we have to create a new object String specClass = classname; final String elementName = node.getNodeName(); if (element2ClassMap.containsKey(elementName)) { specClass = element2ClassMap.get(elementName); } final String spec = getAttribute(node, "spec"); if (spec != null) { specClass = spec; } //if (specClass.indexOf("BEASTInterface") > 0) { // Log.info.println(specClass); //} String clazzName = null; // determine clazzName from specName, taking name spaces in account clazzName = resolveClass(specClass); if (clazzName == null) { // try to create the old-fashioned way by creating the class boolean isDone = false; for (final String nameSpace : nameSpaces) { try { if (!isDone) { Class.forName(nameSpace + specClass); clazzName = nameSpace + specClass; isDone = true; } } catch (ClassNotFoundException e) { // class does not exist -- try another namespace } } } if (clazzName == null) { if (unavailablePacakges.length() > 2) { String msg = "Class " + specClass + " could not be found.\n" + (unavailablePacakges.contains(",") ? "This XML requires the following packages that are not installed: " : "This XML requires the following package that is not installed: ") + unavailablePacakges + "\n" + "See http://beast2.org/managing-packages/ for details on how to install packages.\n" + "Or perhaps there is a typo in spec and you meant " + XMLParserUtils.guessClass(specClass) + "?"; throw new XMLParserException(node, msg, 1018); } throw new XMLParserException(node, "Class could not be found. Did you mean " + XMLParserUtils.guessClass(specClass) + "?", 1017); // throw new ClassNotFoundException(specClass); } // sanity check try { Class<?> clazz = Class.forName(clazzName); if (!BEASTInterface.class.isAssignableFrom(clazz)) { throw new XMLParserException(node, "Expected object to be instance of BEASTObject", 108); } } catch (ClassNotFoundException e1) { // should never happen since clazzName is in the list of classes collected by the AddOnManager e1.printStackTrace(); throw new RuntimeException(e1); } // process inputs List<NameValuePair> inputInfo = parseInputs(node, clazzName); BEASTInterface beastObject = createBeastObject(node, id, clazzName, inputInfo); // initialise if (needsInitialisation) { try { beastObject.validateInputs(); beastObjectsWaitingToInit.add(beastObject); nodesWaitingToInit.add(node); } catch (IllegalArgumentException e) { // next lines for debugging only //beastObject.validateInputs(); //beastObject.initAndValidate(); e.printStackTrace(); throw new XMLParserException(node, "validate and intialize error: " + e.getMessage(), 110); } } return beastObject; } // createObject /** create BEASTInterface either using Inputs, or using annotated constructor **/ private BEASTInterface createBeastObject(Node node, String ID, String clazzName, List<NameValuePair> inputInfo) throws XMLParserException { BEASTInterface beastObject = useAnnotatedConstructor(node, ID, clazzName, inputInfo); if (beastObject != null) { return beastObject; } // create new instance using class name Object o = null; try { Class<?> c = Class.forName(clazzName); o = c.newInstance(); } catch (InstantiationException e) { // we only get here when the class exists, but cannot be // created for instance because it is abstract throw new XMLParserException(node, "Cannot instantiate class. Please check the spec attribute.", 1006); } catch (ClassNotFoundException e) { // ignore -- class was found in beastObjectNames before } catch (IllegalAccessException e) { // T O D O Auto-generated catch block e.printStackTrace(); throw new XMLParserException(node, "Cannot access class. Please check the spec attribute.", 1011); } // set id beastObject = (BEASTInterface) o; beastObject.setID(ID); // hack required to make log-parsing easier if (o instanceof State) { m_state = (State) o; } // process inputs for annotated constructors for (NameValuePair pair : inputInfo) { if (pair.value instanceof BEASTInterface) { setInput(node, beastObject, pair.name, (BEASTInterface) pair.value); } else if (pair.value instanceof String) { setInput(node, beastObject, pair.name, (String) pair.value); } else { throw new RuntimeException("Programmer error: value should be String or BEASTInterface"); } } // fill in missing inputs, if an input provider is available try { if (requiredInputProvider != null) { for (Input<?> input : beastObject.listInputs()) { if (input.get() == null && input.getRule() == Validate.REQUIRED) { Object o2 = requiredInputProvider.createInput(beastObject, input, partitionContext); if (o2 != null) { input.setValue(o2, beastObject); } } } } } catch (Exception e) { throw new XMLParserException(node, e.getMessage(), 1008); } // sanity check: all attributes should be valid input names if (!(beastObject instanceof Map)) { for (NameValuePair pair : inputInfo) { String name = pair.name; if (!(name.equals("id") || name.equals("idref") || name.equals("spec") || name.equals("name"))) { try { beastObject.getInput(name); } catch (Exception e) { throw new XMLParserException(node, e.getMessage(), 1009); } } } } // make sure object o is in outputs of inputs for (NameValuePair pair : inputInfo) { if (pair.value instanceof BEASTInterface) { ((BEASTInterface) pair.value).getOutputs().add((BEASTInterface) o); } } register(node, beastObject); return beastObject; } @SuppressWarnings({ "rawtypes", "unchecked" }) private BEASTInterface useAnnotatedConstructor(Node node, String _id, String clazzName, List<NameValuePair> inputInfo) throws XMLParserException { Class<?> clazz = null; try { clazz = Class.forName(clazzName); } catch (ClassNotFoundException e) { // cannot get here, since we checked the class existed before e.printStackTrace(); } Constructor<?>[] allConstructors = clazz.getDeclaredConstructors(); for (Constructor<?> ctor : allConstructors) { Annotation[][] annotations = ctor.getParameterAnnotations(); List<Param> paramAnnotations = new ArrayList<>(); for (Annotation [] a0 : annotations) { for (Annotation a : a0) { if (a instanceof Param) { paramAnnotations.add((Param) a); } } } for (NameValuePair pair : inputInfo) { pair.processed = false; } Class<?>[] types = ctor.getParameterTypes(); //Type[] gtypes = ctor.getGenericParameterTypes(); if (types.length > 0 && paramAnnotations.size() == types.length) { try { Object [] args = new Object[types.length]; for (int i = 0; i < types.length; i++) { Param param = paramAnnotations.get(i); Type type = types[i]; if (type.getTypeName().equals("java.util.List")) { if (args[i] == null) { // no need to parameterise list due to type erasure args[i] = new ArrayList(); } List<Object> values = getListOfValues(param, inputInfo); ((List)args[i]).addAll(values); } else { args[i] = getValue(param, types[i], inputInfo); // deal with the case where the Input type has a String constructor // and the args[i] is a String -- we need to invoke the String constructor if (args[i].getClass().equals(String.class) && types[i] != String.class) { for (Constructor<?> argctor : types[i].getDeclaredConstructors()) { Class<?>[] argtypes = argctor.getParameterTypes(); if (argtypes.length == 1 && argtypes[0] == String.class) { Object o = argctor.newInstance(args[i]); args[i] = o; break; } } } } } // ensure all inputs are used boolean allUsed = true; for (NameValuePair pair : inputInfo) { if (!pair.processed) { allUsed= false; } } if (allUsed) { try { Object o = ctor.newInstance(args); BEASTInterface beastObject = (BEASTInterface) o; register(node, beastObject); return beastObject; } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new XMLParserException(node, "Could not create object: " + e.getMessage(), 1012); } } } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { // we get here when a param value cannot be constructed from a default value // let's try the next constructor (if any) } } } return null; } private Object getValue(Param param, Class<?> type, List<NameValuePair> inputInfo) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { for (NameValuePair pair : inputInfo) { if (pair.name.equals(param.name())) { pair.processed = true; if (type.isAssignableFrom(Integer.class)) { return Integer.parseInt((String) pair.value); } if (type.isAssignableFrom(Double.class)) { return Double.parseDouble((String) pair.value); } return pair.value; } } // could not find Param entry in inputInfo // check if this parameter is required or optional if (!param.optional()) { throw new IllegalArgumentException(); } // try using a String constructor of the default value Constructor<?> ctor; String value = param.defaultValue(); Object v = value; try { ctor = type.getDeclaredConstructor(String.class); } catch (NoSuchMethodException e) { // we get here if there is not String constructor // try integer constructor instead try { if (value.startsWith("0x")) { v = Integer.parseInt(value.substring(2), 16); } else { v = Integer.parseInt(value); } ctor = type.getDeclaredConstructor(int.class); } catch (NumberFormatException e2) { // could not parse as integer, try double instead v = Double.parseDouble(value); ctor = type.getDeclaredConstructor(double.class); } } ctor.setAccessible(true); final Object o = ctor.newInstance(v); return o; } static List<Object> getListOfValues(Param param, List<NameValuePair> inputInfo) { List<Object> values = new ArrayList<>(); for (NameValuePair pair : inputInfo) { if (pair.name.equals(param.name())) { values.add(pair.value); pair.processed = true; } } return values; } @Deprecated // use XMLParserUtils.getLevenshteinDistance instead public static int getLevenshteinDistance(final String s, final String t) { return XMLParserUtils.getLevenshteinDistance(s, t); } private List<NameValuePair> parseInputs(Node node, String clazzName) throws XMLParserException { List<NameValuePair> inputInfo = new ArrayList<>(); // first, process attributes NamedNodeMap atts = node.getAttributes(); if (atts != null) { for (int i = 0; i < atts.getLength(); i++) { final String name = atts.item(i).getNodeName(); if (!(name.equals("id") || name.equals("idref") || name.equals("spec") || name.equals("name"))) { final String value = atts.item(i).getNodeValue(); if (value.startsWith("@")) { final String idRef = value.substring(1); final Element element = doc.createElement("input"); element.setAttribute("idref", idRef); // add child in case things go belly up, and an XMLParserException is thrown node.appendChild(element); final BEASTInterface beastObject = createObject(element, BEAST_INTERFACE_CLASS); // it is save to remove the element now node.removeChild(element); inputInfo.add(new NameValuePair(name, beastObject)); } else { inputInfo.add(new NameValuePair(name, value)); } } } } // process element nodes final NodeList children = node.getChildNodes(); int childElements = 0; String text = ""; for (int i = 0; i < children.getLength(); i++) { final Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { final String element = child.getNodeName(); // resolve name of the input String name = getAttribute(child, "name"); if (name == null) { name = element; } // resolve base class String specClass = BEAST_INTERFACE_CLASS; if (element2ClassMap.containsKey(element)) { specClass = element2ClassMap.get(element); } final String spec = getAttribute(child, "spec"); if (spec != null) { specClass = spec; } String classname = null; // determine clazzName from specName, taking name spaces in account classname = resolveClass(specClass); if (classname == null) { classname = specClass; } // test for special cases: <xyz>value</xyz> and <input name="xyz">value</input> // where value is a string boolean done = false; atts = child.getAttributes(); if (atts.getLength() == 0 || (element.equals("input") && atts.getLength() == 1 && name != null)) { NodeList grantchildren = child.getChildNodes(); boolean hasElements = false; for (int j = 0; j < grantchildren.getLength(); j++) { if (grantchildren.item(j).getNodeType() == Node.ELEMENT_NODE) { hasElements = true; break; } } if (!hasElements) { String content = child.getTextContent(); inputInfo.add(new NameValuePair(name, content)); done = true; } } // create object from element, if not already done so if (!done) { final BEASTInterface childItem = createObject(child, classname); if (childItem != null) { inputInfo.add(new NameValuePair(name, childItem)); } } childElements++; } else if (child.getNodeType() == Node.CDATA_SECTION_NODE || child.getNodeType() == Node.TEXT_NODE) { text += child.getTextContent(); } } if (!text.matches("\\s*")) { inputInfo.add(new NameValuePair("value", text)); } if (childElements == 0) { final String content = node.getTextContent(); if (content != null && content.length() > 0 && content.replaceAll("\\s", "").length() > 0) { try { inputInfo.add(new NameValuePair("value", content)); } catch (Exception e) { // } } } return inputInfo; } // setInputs private String resolveClass(String specClass) { for (String nameSpace : nameSpaces) { if (XMLParserUtils.beastObjectNames.contains(nameSpace + specClass)) { String clazzName = nameSpace + specClass; return clazzName; } } for (String nameSpace : nameSpaces) { try { if (Class.forName(nameSpace + specClass) != null) { String clazzName = nameSpace + specClass; return clazzName; } } catch (ClassNotFoundException e) { // ignore } } return null; } void setInput(final Node node, final BEASTInterface beastObject, final String name, final BEASTInterface beastObject2) throws XMLParserException { try { final Input<?> input = beastObject.getInput(name); // test whether input was not set before, this is done by testing whether input has default value. // for non-list inputs, this should be true if the value was not already set before // for list inputs this is always true. if (input.get() == input.defaultValue) { beastObject.setInputValue(name, beastObject2); } else { throw new XMLParserException(node, "\nMultiple entries for input \"" + input.getName() + "\" but only single entry expected " + "in element \"" + node.getNodeName() + "\"", 130); } return; } catch (XMLParserException e) { throw e; } catch (Exception e) { if (name.equals("xml:base")) { // ignore xml:base attributes introduces by XML entities return; } if (e.getMessage().contains("101")) { String type = "?"; try { type = beastObject.getInput(name).getType().getName().replaceAll(".*\\.", ""); } catch (Exception e2) { // TODO: handle exception } throw new XMLParserException(node, e.getMessage() + " expected '" + type + "' but got '" + beastObject2.getClass().getName().replaceAll(".*\\.", "") + "'" , 123); } else { throw new XMLParserException(node, e.getMessage(), 130); } } //throw new XMLParserException(node, "no such input '"+name+"' for element <" + node.getNodeName() + ">", 167); } void setInput(final Node node, final BEASTInterface beastObject, final String name, final String value) throws XMLParserException { try { beastObject.setInputValue(name, value); return; } catch (Exception e) { if (name.equals("xml:base")) { // ignore xml:base attributes introduces by XML entities return; } try { beastObject.setInputValue(name, value); } catch (Exception e1) { // TODO Auto-generated catch block e1.printStackTrace(); } throw new XMLParserException(node, e.getMessage(), 124); } //throw new XMLParserException(node, "no such input '"+name+"' for element <" + node.getNodeName() + ">", 168); } /** * records id in IDMap, for ease of retrieving beast objects associated with idrefs * */ void register(final Node node, final BEASTInterface beastObject) { final String id = getID(node); if (id != null) { IDMap.put(id, beastObject); } } public static String getID(final Node node) { return getAttribute(node, "id"); } // getID public static String getIDRef(final Node node) { return getAttribute(node, "idref"); } // getIDRef /** * get string value of attribute with given name * as opposed to double or integer value (see methods below) * */ public static String getAttribute(final Node node, final String attName) { final NamedNodeMap atts = node.getAttributes(); if (atts == null) { return null; } for (int i = 0; i < atts.getLength(); i++) { final String name = atts.item(i).getNodeName(); if (name.equals(attName)) { return atts.item(i).getNodeValue(); } } return null; } // getAttribute /** * get integer value of attribute with given name * */ public static int getAttributeAsInt(final Node node, final String attName) { final String att = getAttribute(node, attName); if (att == null) { return -1; } return Integer.parseInt(att); } /** * get double value of attribute with given name * */ public static double getAttributeAsDouble(final Node node, final String attName) { final String att = getAttribute(node, attName); if (att == null) { return Double.MAX_VALUE; } return Double.parseDouble(att); } /** * test whether a node contains a attribute with given name * */ boolean hasAtt(final Node node, final String attributeName) { final NamedNodeMap atts = node.getAttributes(); if (atts != null) { for (int i = 0; i < atts.getLength(); i++) { final String name = atts.item(i).getNodeName(); if (name.equals(attributeName)) { return true; } } } return false; } public interface RequiredInputProvider { Object createInput(BEASTInterface beastObject, Input<?> input, PartitionContext context); } public void setRequiredInputProvider(final RequiredInputProvider provider, final PartitionContext context) { requiredInputProvider = provider; partitionContext = context; } /** * parses file and formats it using the XMLProducer * */ public static void main(final String[] args) { try { // redirect stdout to stderr final PrintStream out = System.out; System.setOut(System.err); // parse the file final XMLParser parser = new XMLParser(); final BEASTInterface beastObject = parser.parseFile(new File(args[0])); // restore stdout System.setOut(out); System.out.println(new XMLProducer().toXML(beastObject)); } catch (Exception e) { e.printStackTrace(); } } } // class XMLParser