/* * File XMLProducer.java * * Copyright (C) 2010 Remco Bouckaert remco@cs.auckland.ac.nz * * This file is part of BEAST 2. * 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 java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; 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.w3c.dom.Attr; 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.BEASTVersion2; import beast.core.BEASTInterface; import beast.core.Input; /** * converts MCMC plug in into XML, i.e. does the reverse of XMLParser * but tries to prettify the XML as well. */ public class XMLProducer extends XMLParser { /** * list of objects already converted to XML, so an idref suffices */ HashSet<BEASTInterface> isDone; Map<BEASTInterface, Set<String>> isInputsDone; /** * list of IDs of elements produces, used to prevent duplicate ID generation */ HashSet<String> IDs; /** * #spaces before elements in XML * */ int indent; final public static String DEFAULT_NAMESPACE = "beast.core:beast.evolution.alignment:beast.evolution.tree.coalescent:beast.core.util:beast.evolution.nuc:beast.evolution.operators:beast.evolution.sitemodel:beast.evolution.substitutionmodel:beast.evolution.likelihood"; //final public static String DO_NOT_EDIT_WARNING = "DO NOT EDIT the following machine generated text, they are used in Beauti"; public XMLProducer() { super(); } /** * Main entry point for this class * Given a plug-in, produces the XML in BEAST 2.0 format * representing the plug-in. This assumes beast object is Runnable */ public String toXML(BEASTInterface beastObject) { return toXML(beastObject, new ArrayList<>()); } public String toXML(BEASTInterface beastObject, Collection<BEASTInterface> others) { try { StringBuffer buf = new StringBuffer(); Set<String> requiredPacakges = AddOnManager.getPackagesAndVersions(beastObject); String required = requiredPacakges.toString(); required = required.substring(1, required.length() - 1); required = required.replaceAll(", ", ":"); buf.append("<" + XMLParser.BEAST_ELEMENT + " version='" + new BEASTVersion2().getMajorVersion() + "'" + " required='" + required + "'" + " namespace='" + DEFAULT_NAMESPACE + "'>\n"); for (String element : element2ClassMap.keySet()) { if (!reservedElements.contains(element)) { buf.append("<map name='" + element + "'>" + element2ClassMap.get(element) +"</map>\n"); } } buf.append("\n\n"); isDone = new HashSet<>(); isInputsDone = new HashMap<>(); IDs = new HashSet<>(); indent = 0; beastObjectToXML(beastObject, buf, null, true); String endBeastString = "</" + XMLParser.BEAST_ELEMENT + ">"; buf.append(endBeastString); //return buf.toString(); // beautify XML hierarchy String xml = cleanUpXML(buf.toString(), m_sXMLBeuatifyXSL); // TODO: fix m_sIDRefReplacementXSL script to deal with nested taxon sets // String xml2 = cleanUpXML(xml, m_sIDRefReplacementXSL); String xml2 = xml; xml = findPlates(xml2); // beatify by applying name spaces to spec attributes String[] nameSpaces = DEFAULT_NAMESPACE.split(":"); for (String nameSpace : nameSpaces) { xml = xml.replaceAll("spec=\"" + nameSpace + ".", "spec=\""); } buf = new StringBuffer(); if (others.size() > 0) { for (BEASTInterface beastObject2 : others) { if (!IDs.contains(beastObject2.getID())) { beastObjectToXML(beastObject2, buf, null, false); } } } int endIndex = xml.indexOf(endBeastString); String extras = buf.toString(); // prevent double -- inside XML comment, this can happen in sequences extras = extras.replaceAll("--","- - "); xml = xml.substring(0, endIndex) //+ "\n\n<!-- " + DO_NOT_EDIT_WARNING + " \n\n" + //extras + "\n\n-->\n\n" + endBeastString; xml = xml.replaceAll("xmlns=\"http://www.w3.org/TR/xhtml1/strict\"", ""); xml = dedupName(xml); xml = sortTags(xml); //insert newlines in alignments int k = xml.indexOf("<data "); StringBuffer buf2 = new StringBuffer(xml); while (k > 0) { while (xml.charAt(k) != '>') { if (xml.charAt(k) == ' ' && !xml.startsWith("idref", k+1)) { buf2.setCharAt(k, '\n'); } k++; } k = xml.indexOf("<data ", k + 1); } return buf2.toString(); } catch (Exception e) { e.printStackTrace(); return null; } } // toXML // ensure attributes are ordered so that id goes first, then spec, then remainder of attributes private String sortTags(String xml) { //if (true) return xml; String [] strs = xml.split("<"); StringBuilder bf = new StringBuilder(); bf.append(strs[0]); for (int i = 1; i < strs.length; i++) { String str = strs[i]; String [] ss = str.split(">"); boolean [] isShortEnd = new boolean[ss.length]; for (int j = 0; j < ss.length; j++) { if (ss[j].endsWith("/")) { isShortEnd[j] = true; ss[j] = ss[j].substring(0, ss[j].length() - 1); } } String [] strs2 = split(ss[0]); int spec = 0; while (spec < strs2.length && !strs2[spec].startsWith("spec=")) { spec++; } int iD = 0; while (iD < strs2.length && !strs2[iD].startsWith("id=")) { iD++; } bf.append('<'); if (strs2[0] != null) bf.append(strs2[0]); if (iD < strs2.length) { bf.append(' '); bf.append(strs2[iD]); } if (spec < strs2.length) { bf.append(' '); bf.append(strs2[spec]); } for (int j = 1; j < strs2.length; j++) { if (j != iD && j != spec) { bf.append(' '); if (strs2[j] != null) bf.append(strs2[j]); } } for (int k = 1; k < ss.length; k++) { if (isShortEnd[k-1]) { bf.append('/'); } bf.append('>'); bf.append(ss[k]); } if (ss.length == 1 && str != null && str.endsWith(">")) { bf.append('>'); } } return bf.toString(); } // since str.split(" "): does not match trailing spaces, we need to split by hand // also, attributes with spaces in them should not be split, e.g. <x id="a b"/> should be split in 2, not 3 String [] split(String str) { List<String> s = new ArrayList<>(); StringBuilder buf = new StringBuilder(); int i = 0; while (i < str.length()) { char c = str.charAt(i); if (c == ' ') { String str2 = buf.toString(); if ((str2.contains("='") && !str2.endsWith("'")) || (str2.contains("=\"") && !str2.endsWith("\""))) { buf.append(c); } else { s.add(str2); buf = new StringBuilder(); } } else { buf.append(c); } i++; } s.add(buf.toString()); return s.toArray(new String []{}); } String dedupName(String xml) { // replace <$x name="$y" idref="$z"/> and <$x idref="$z" name="$y"/> // with <$y idref="$z"/> StringBuilder sb = new StringBuilder(); int i = -1; while (++i < xml.length()) { char c = xml.charAt(i); if (c == '<') { StringBuilder tag = new StringBuilder(); tag.append(c); while (((c = xml.charAt(++i)) != ' ') && (c != '/') && c != '>') { tag.append(c); } if (c != '/' && c != '>') { StringBuilder tag2 = new StringBuilder(); while ((c = xml.charAt(++i)) != '=') { tag2.append(c); } if (tag2.toString().equals("name")) { ++i; StringBuilder value2 = new StringBuilder(); while ((c = xml.charAt(++i)) != '"') { value2.append(c); } StringBuilder tag3 = new StringBuilder(); c = xml.charAt(++i); if (c != '>') { if (c == '/') { tag3.append(c); } while (((c = xml.charAt(++i)) != '=') && (c != '/') && (c != '>')) { tag3.append(c); } } if (c != '/' && c != '>' && tag3.toString().equals("idref")) { tag3.append(c); tag3.append(xml.charAt(++i)); while ((c = xml.charAt(++i)) != '"') { tag3.append(c); } sb.append('<'); sb.append(value2); sb.append('='); sb.append(tag3); sb.append("/>"); while ((c = xml.charAt(++i)) != '>') {} } else { sb.append(tag); sb.append(' '); sb.append(tag2); sb.append("=\""); sb.append(value2); sb.append('"'); sb.append(' '); sb.append(tag3); sb.append(c); } } else if (tag2.toString().equals("idref")) { tag2.append(c); tag2.append(xml.charAt(++i)); while (((c = xml.charAt(++i)) != ' ') && (c != '/') && c != '>') { tag2.append(c); } if (c != '/' && c != '>') { StringBuilder tag3 = new StringBuilder(); while ((c = xml.charAt(++i)) != '=') { tag3.append(c); } if (tag3.toString().equals("name")) { ++i; StringBuilder value2 = new StringBuilder(); while ((c = xml.charAt(++i)) != '"') { value2.append(c); } sb.append('<'); sb.append(value2); sb.append(' '); sb.append(tag2); sb.append("/>"); while ((c = xml.charAt(++i)) != '>') {} } else { sb.append(tag); sb.append(' '); sb.append(tag2); sb.append(' '); sb.append(tag3); sb.append(c); } } else { sb.append(tag); sb.append(' '); sb.append(tag2); sb.append(c); } } else { sb.append(tag); sb.append(' '); sb.append(tag2); sb.append(c); } } else { sb.append(tag); sb.append(c); } } else { sb.append(c); } } return sb.toString(); } /** * like toXML() but without the assumption that beast object is Runnable * */ public String modelToXML(BEASTInterface beastObject) { try { String xML0 = toRawXML(beastObject); String xml = cleanUpXML(xML0, m_sSupressAlignmentXSL); // TODO: fix m_sIDRefReplacementXSL script to deal with nested taxon sets //String xml2 = cleanUpXML(xml, m_sIDRefReplacementXSL); String xml2 = xml; xml = findPlates(xml2); xml = xml.replaceAll("<\\?xml version=\"1.0\" encoding=\"UTF-8\"\\?>", ""); xml = xml.replaceAll("\\n\\s*\\n", "\n"); return xml; } catch (Exception e) { e.printStackTrace(); return null; } } // toXML /** * like modelToXML, but without the cleanup * */ public String toRawXML(BEASTInterface beastObject) { return toRawXML(beastObject, null); } // toRawXML /** * like modelToXML, but without the cleanup * * For beast object without name */ public String toRawXML(BEASTInterface beastObject, String name) { try { StringBuffer buf = new StringBuffer(); isDone = new HashSet<>(); isInputsDone = new HashMap<>(); IDs = new HashSet<>(); indent = 0; beastObjectToXML(beastObject, buf, name, false); return buf.toString(); } catch (Exception e) { e.printStackTrace(); return null; } } // toRawXML public String stateNodeToXML(BEASTInterface beastObject) { try { StringBuffer buf = new StringBuffer(); //buf.append("<" + XMLParser.BEAST_ELEMENT + " version='2.0'>\n"); isDone = new HashSet<>(); IDs = new HashSet<>(); indent = 0; beastObjectToXML(beastObject, buf, null, false); return buf.toString(); } catch (Exception e) { e.printStackTrace(); return null; } } /** * Applies XSL script (specified in m_sXSL) to make XML a bit * nicer by removing unused IDs and moving data, beast.tree and likelihood * outside MCMC element. * Tries to compress common parts into plates. */ String cleanUpXML(String xml, String xsl) throws TransformerException { StringWriter strWriter = new StringWriter(); Reader xmlInput = new StringReader(xml); javax.xml.transform.Source xmlSource = new javax.xml.transform.stream.StreamSource(xmlInput); Reader xslInput = new StringReader(xsl); javax.xml.transform.Source xsltSource = new javax.xml.transform.stream.StreamSource(xslInput); javax.xml.transform.Result result = new javax.xml.transform.stream.StreamResult(strWriter); // create an instance of TransformerFactory javax.xml.transform.TransformerFactory transFact = javax.xml.transform.TransformerFactory.newInstance(); javax.xml.transform.Transformer trans = transFact.newTransformer(xsltSource); trans.transform(xmlSource, result); String xml2 = strWriter.toString(); return xml2; } // compress parts into plates String findPlates(String xml) throws SAXException, IOException, ParserConfigurationException, TransformerException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); doc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); doc.normalize(); Node topNode = doc.getElementsByTagName("*").item(0); findPlates(topNode); //create string from xml tree StringWriter sw = new StringWriter(); StreamResult result = new StreamResult(sw); DOMSource source = new DOMSource(doc); TransformerFactory factory2 = TransformerFactory.newInstance(); Transformer transformer = factory2.newTransformer(); transformer.transform(source, result); return sw.toString(); } /** * tries to compress XML into plates * */ void findPlates(Node node) { NodeList children = node.getChildNodes(); for (int childIndex = 0; childIndex < children.getLength(); childIndex++) { Node child = children.item(childIndex); if (child.getNodeType() == Node.ELEMENT_NODE) { List<Node> comparables = new ArrayList<>(); for (int siblingNr = childIndex + 1; siblingNr < children.getLength(); siblingNr++) { if (children.item(siblingNr).getNodeType() == Node.ELEMENT_NODE) { Node sibling = children.item(siblingNr); if (comparable(child, sibling, ".p1", ".p" + (comparables.size() + 2))) { comparables.add(sibling); } else { // break siblingNr = children.getLength(); } } } if (comparables.size() > 0) { // TODO: FIX THIS SO THAT NOT AN ARBITRARY `1' is used to generate the plate // we can make a plate now // String rangeString = "1"; // int k = 2; // for (Node sibling : comparables) { // rangeString += "," + k++; // sibling.getParentNode().removeChild(sibling); // } // makePlate(child, "1", "n", rangeString); } } } // recurse to lower levels children = node.getChildNodes(); for (int childIndex = 0; childIndex < children.getLength(); childIndex++) { findPlates(children.item(childIndex)); } } // findPlates /** * replace node element by a plate element with variable var and range rangeString * */ void makePlate(Node node, String pattern, String var, String rangeString) { Element plate = doc.createElement("plate"); plate.setAttribute("var", var); plate.setAttribute("range", rangeString); String indent = node.getPreviousSibling().getTextContent(); replace(node, pattern, var); node.getParentNode().replaceChild(plate, node); plate.appendChild(doc.createTextNode(indent + " ")); plate.appendChild(node); plate.appendChild(doc.createTextNode(indent)); } /** * recursively replace all attribute values containing the pattern with variable var * */ void replace(Node node, String pattern, String var) { NamedNodeMap atts = node.getAttributes(); if (atts != null) { for (int i = 0; i < atts.getLength(); i++) { Attr attr = (Attr) atts.item(i); String valueString = attr.getValue().replaceAll(pattern, "\\$\\(" + var + "\\)"); attr.setValue(valueString); } } NodeList children = node.getChildNodes(); for (int childIndex = 0; childIndex < children.getLength(); childIndex++) { Node child = children.item(childIndex); replace(child, pattern, var); } } /** * check if two XML nodes are the same, when pattern1 is replaced by pattothersern2 * */ boolean comparable(Node node1, Node node2, String pattern1, String pattern2) { // compare name if (!node1.getNodeName().equals(node2.getNodeName())) { return false; } // compare text if (!node1.getTextContent().trim().equals(node2.getTextContent().trim())) { return false; } // compare attributes NamedNodeMap atts = node1.getAttributes(); NamedNodeMap atts2 = node2.getAttributes(); if (atts.getLength() != atts2.getLength()) { return false; } for (int i = 0; i < atts.getLength(); i++) { Attr attr = (Attr) atts.item(i); String name = attr.getName(); String valueString = attr.getValue(); Node att = atts2.getNamedItem(name); if (att == null) { return false; } String valueString2 = ((Attr) att).getValue(); if (!valueString.equals(valueString2)) { valueString = valueString.replaceAll(pattern1, "\\$\\(n\\)"); valueString2 = valueString2.replaceAll(pattern2, "\\$\\(n\\)"); if (!valueString.equals(valueString2)) { return false; } } } // compare children NodeList children = node1.getChildNodes(); NodeList children2 = node2.getChildNodes(); for (int childIndex = 0; childIndex < children.getLength(); childIndex++) { Node child = children.item(childIndex); if (child.getNodeType() == Node.ELEMENT_NODE) { String name = child.getNodeName(); boolean isMatch = false; for (int childIndex2 = 0; !isMatch && childIndex2 < children2.getLength(); childIndex2++) { Node child2 = children2.item(childIndex2); if (child2.getNodeType() == Node.ELEMENT_NODE && name.equals(child2.getNodeName())) { isMatch = comparable(child, child2, pattern1, pattern2); } } if (!isMatch) { return false; } } } return true; } // comparable /** * XSL stylesheet for cleaning up bits and pieces of the vanilla XML * in order to make it more readable * */ String m_sXMLBeuatifyXSL = "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns='http://www.w3.org/TR/xhtml1/strict'>\n" + "\n" + "<xsl:output method='xml' indent='yes'/>\n" + "\n" + "<xsl:template match='beast'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*'/>\n" + " <xsl:text> </xsl:text>\n" + " <xsl:apply-templates select='//data[not(@idref)]' mode='copy'/>\n" + " <xsl:text> </xsl:text>\n" + " <xsl:apply-templates select='//beast.tree[not(@idref)]' mode='copy'/>\n" + " <xsl:text> </xsl:text>\n" + " <xsl:apply-templates select='//distribution[not(@idref) and not(ancestor::distribution)]' mode='copy'/>\n" + " <xsl:text> </xsl:text>\n" + " <xsl:apply-templates select='node()'/> \n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='*' mode='copy'>\n" + " <xsl:copy>\n" + " <xsl:attribute name='id'>\n" + " <xsl:value-of select='@id'/>\n" + " </xsl:attribute>\n" + " <xsl:apply-templates select='@*|node()'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='data|beast.tree|distribution[not(ancestor::distribution)]'>\n" + " <xsl:copy>\n" + " <xsl:attribute name='idref'>\n" + " <xsl:choose>\n" + " <xsl:when test='@idref!=\"\"'><xsl:value-of select='@idref'/></xsl:when>\n" + " <xsl:otherwise><xsl:value-of select='@id'/></xsl:otherwise>\n" + " </xsl:choose>\n" + " </xsl:attribute>\n" + " <xsl:apply-templates select='@name'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='input'>\n" + " <xsl:element name='{@name}'>" + " <xsl:apply-templates select='node()|@*[name()!=\"name\"]'/>" + " </xsl:element>\n" + "</xsl:template>\n" + "<xsl:template match='log/log'>\n" + " <xsl:copy><xsl:apply-templates select='*[@*!=\"\"]'/> </xsl:copy>\n" + "</xsl:template>\n" + "\n" + // Better not suppress unused id's; used for example in reporting Operators //"<xsl:template match='@id'>\n" + //" <xsl:if test='//@idref=. or not(contains(../@spec,substring(.,string-length(.)-2)))'>\n" + //" <xsl:copy/>\n" + //" </xsl:if>\n" + //"</xsl:template>\n" + "\n" + "<xsl:template match='@*|node()'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*|node()'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "</xsl:stylesheet>\n"; /** * script to reduce elements of the form <name idref='xyz'/> to name='@xyz' attributes * */ String m_sIDRefReplacementXSL = "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns='http://www.w3.org/TR/xhtml1/strict'>\n" + "\n" + "<xsl:output method='xml' indent='yes'/>\n" + "\n" + "<xsl:template match='beast'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*|node()'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='node()'>\n" + " <xsl:choose>\n" + " <xsl:when test='count(@idref)=1 and count(@name)=1 and count(@*)=2'>\n" + " <xsl:element name='{@name}'>\n" + " <xsl:attribute name='idref'>\n" + " <xsl:value-of select='@idref'/>\n" + " </xsl:attribute>\n" + " </xsl:element>\n" + " </xsl:when>\n" + " <xsl:when test='not(count(@idref)=1 and count(@*)=1)'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*'/>\n" + " <xsl:for-each select='*'>\n" + " <xsl:if test='count(@idref)=1 and count(@*)=1'>\n" + " <xsl:attribute name='{name()}'>@<xsl:value-of select='@idref'/></xsl:attribute>\n" + " </xsl:if>\n" + " </xsl:for-each>\n" + " <xsl:apply-templates/>\n" + " </xsl:copy>\n" + " </xsl:when>\n" + " </xsl:choose>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='@*'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*|node()'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "</xsl:stylesheet>"; String s = "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns='http://www.w3.org/TR/xhtml1/strict'>\n" + "\n" + "<xsl:output method='xml' indent='yes'/>\n" + "\n" + "<xsl:template match='beast'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*|node()'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='node()'>\n" + " <xsl:if test='not(count(@idref)=1 and count(@*)=1)'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*'/>\n" + " <xsl:for-each select='*'>\n" + " <xsl:if test='count(@idref)=1 and count(@*)=1'>\n" + " <xsl:attribute name='{name()}'>@<xsl:value-of select='@idref'/></xsl:attribute>\n" + " </xsl:if>\n" + " </xsl:for-each>\n" + " <xsl:apply-templates/>\n" + " </xsl:copy>\n" + " </xsl:if>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='@*'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*|node()'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "</xsl:stylesheet>\n"; /** * XSL stylesheet for suppressing alignment* */ String m_sSupressAlignmentXSL = "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns='http://www.w3.org/TR/xhtml1/strict'>\n" + "\n" + "<xsl:output method='xml'/>\n" + "\n" + "<xsl:template match='data'/>\n" + "\n" + "<xsl:template match='input[@name]'>\n" + " <xsl:element name='{@name}'>" + " <xsl:apply-templates select='node()|@*[name()!=\"name\"]'/>" + " </xsl:element>\n" + "</xsl:template>\n" + "\n" + "<xsl:template match='@*|node()'>\n" + " <xsl:copy>\n" + " <xsl:apply-templates select='@*|node()'/>\n" + " </xsl:copy>\n" + "</xsl:template>\n" + "\n" + "</xsl:stylesheet>\n"; /** * produce elements for a beast object with name name, putting results in buf. * It tries to create XML conforming to the XML transformation rules (see XMLParser) * that is moderately readable. * @throws ClassNotFoundException */ @SuppressWarnings("rawtypes") void beastObjectToXML(BEASTInterface beastObject, StringBuffer buf, String name, boolean isTopLevel) throws ClassNotFoundException { // determine element name, default is input, otherswise find one of the defaults String elementName = "input"; for (String key : element2ClassMap.keySet()) { String className = element2ClassMap.get(key); Class _class = Class.forName(className); if (_class.equals(beastObject.getClass())) { elementName = key; } } // if (beastObject instanceof Alignment) { // elementName = XMLParser.DATA_ELEMENT; // } // if (beastObject instanceof Sequence) { // elementName = XMLParser.SEQUENCE_ELEMENT; // } // if (beastObject instanceof State) { // elementName = XMLParser.STATE_ELEMENT; // } // if (beastObject instanceof Distribution) { // elementName = XMLParser.DISTRIBUTION_ELEMENT; // } // if (beastObject instanceof Logger) { // elementName = XMLParser.LOG_ELEMENT; // } // if (beastObject instanceof Operator) { // elementName = XMLParser.OPERATOR_ELEMENT; // } // if (beastObject instanceof RealParameter) { // elementName = XMLParser.REAL_PARAMETER_ELEMENT; // } // if (beastObject instanceof Tree) { // elementName = XMLParser.TREE_ELEMENT; // } if (isTopLevel) { elementName = XMLParser.RUN_ELEMENT; } for (int i = 0; i < indent; i++) { buf.append(" "); } indent++; // open element buf.append("<").append(elementName); if (beastObject.getID() == null) { String id = beastObject.getClass().getName(); if (id.contains(".")) { id = id.substring(id.lastIndexOf('.') + 1); } if (IDs.contains(id)) { int k = 1; while (IDs.contains(id + k)) { k++; } id = id + k; } beastObject.setID(id); } boolean skipInputs = false; // isDone.contains(beastObject) fails when BEASTObjects override equals(), so use a stream with == instead if (isDone.stream().anyMatch(x -> x == beastObject)) { // XML is already produced, we can idref it buf.append(" idref='" + normalise(beastObject.getID()) + "'"); skipInputs = true; } else { // see whether a reasonable id can be generated if (beastObject.getID() != null && !beastObject.getID().equals("")) { String id = beastObject.getID(); // ensure ID is unique, if not add index behind uniqueID(id, buf); } isDone.add(beastObject); } String className = beastObject.getClass().getName(); if (skipInputs == false && (!element2ClassMap.containsKey(elementName) || !element2ClassMap.get(elementName).equals(className))) { // only add spec element if it cannot be deduced otherwise (i.e., by idref or default mapping buf.append(" spec='" + className + "'"); } if (name != null && !name.equals(elementName)) { // only add name element if it differs from element = default name buf.append(" name='" + name + "'"); } if (!skipInputs) { // process inputs of this beast object // first, collect values as attributes List<Input<?>> inputs = beastObject.listInputs(); for (Input<?> input : inputs) { Object value = input.get(); inputToXML(input, value, beastObject, buf, true); } // next, collect values as input elements StringBuffer buf2 = new StringBuffer(); for (Input input : inputs) { Object value = input.get(); inputToXML(input, value, beastObject, buf2, false); } if (buf2.length() == 0) { // if nothing was added by the inputs, close element indent--; buf.append("/>\n"); } else { // add contribution of inputs if (buf2.indexOf("<") >= 0) { buf.append(">\n"); buf.append(buf2); indent--; for (int i = 0; i < indent; i++) { buf.append(" "); } } else { buf.append(">"); buf.append(buf2.toString().trim()); indent--; } // add closing element buf.append("</" + elementName + ">\n"); } } else { // close element indent--; buf.append("/>\n"); } if (indent < 2) { buf.append("\n"); } } // pluginToXML // ensure ID is unique, if not add index behind private void uniqueID(String id, StringBuffer buf) { if (IDs.contains(id)) { int k = 1; while (IDs.contains(id + k)) { k++; } id = id + k; } buf.append(" id='" + normalise(id) + "'"); IDs.add(id); } /** * produce XML for an input of a beast object, both as attribute/value pairs for * primitive inputs (if isShort=true) and as individual elements (if isShort=false) * * @param input: name of the input * @param beastObject: beast object to produce this input XML for * @param buf: gets XML results are appended * @param isShort: flag to indicate attribute/value format (true) or element format (false) * @throws ClassNotFoundException */ void inputToXML(Input<?> input, Object value, BEASTInterface beastObject, StringBuffer buf, boolean isShort) throws ClassNotFoundException { //if (input.getName().equals("*")) { // this can happen with beast.core.parameter.Map // and * is not a valid XML attribute name //return; //} if (value != null) { if (value instanceof Map) { // distinguish between List, Map, BEASTInterface and primitive input types if (isShort) { @SuppressWarnings("unchecked") Map<String,?> map = (Map<String,?>) value; // determine label width int whiteSpaceWidth = 0; List<String> keys = new ArrayList<>(); keys.addAll(map.keySet()); Collections.sort(keys); for (String key : keys) { whiteSpaceWidth = Math.max(whiteSpaceWidth, key.length()); } for (String key : map.keySet()) { //buf.append(" <input name='" + key + "'>"); buf.append("\n " + key); for (int k = key.length(); k < whiteSpaceWidth; k++) { buf.append(' '); } buf.append("=\"" + normalise(map.get(key).toString()) + "\""); } } return; } else if (input.getName().startsWith("*")) { // this can happen with private inputs, like in ThreadedTreeLikelihood // and * is not a valid XML attribute name return; } else if (value instanceof List) { if (!isShort) { int k = 0; List<?> list = (List<?>) value; for (Object o2 : list) { if (o2 instanceof BEASTInterface) { beastObjectToXML((BEASTInterface) o2, buf, input.getName(), false); } else { k++; buf.append(o2.toString()); if (k < list.size()) { buf.append(' '); } } } } return; } else if (value instanceof BEASTInterface) { if (!value.equals(input.defaultValue)) { if (isShort && isDone.contains(value)) { buf.append(" " + input.getName() + "='@" + normalise( ((BEASTInterface) value).getID() ) + "'"); if (!isInputsDone.containsKey(beastObject)) { isInputsDone.put(beastObject, new HashSet<>()); } isInputsDone.get(beastObject).add(input.getName()); } if (!isShort && (!isInputsDone.containsKey(beastObject) || !isInputsDone.get(beastObject).contains(input.getName()))) { beastObjectToXML((BEASTInterface) value, buf, input.getName(), false); } } return; } else { if (!value.equals(input.defaultValue)) { // primitive type String valueString = value.toString(); if (isShort) { if (valueString.indexOf('\n') < 0) { buf.append(" " + input.getName() + "='" + normalise(value.toString()) + "'"); } } else { if (valueString.indexOf('\n') >= 0) { for (int j = 0; j < indent; j++) { buf.append(" "); } if (input.getName().equals("value")) { buf.append(normalise(value.toString())); } else { buf.append("<input name='" + input.getName() + "'>" + normalise(value.toString()) + "</input>\n"); } } } } return; } } else { // value=null, no XML to produce return; } } // inputToXML /** convert plain text string to XML string, replacing some entities **/ String normalise(String str) { str = str.replaceAll("&", "&"); str = str.replaceAll("'", "'"); str = str.replaceAll("\"", """); str = str.replaceAll("<", "<"); str = str.replaceAll(">", ">"); return str; } } // class XMLProducer