/*
* XMLParser.java
*
* Copyright (c) 2002-2015 Alexei Drummond, Andrew Rambaut and Marc Suchard
*
* This file is part of BEAST.
* 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 dr.xml;
import dr.inference.model.Likelihood;
import dr.inference.model.Model;
import dr.inference.model.Parameter;
import dr.inferencexml.loggers.LoggerParser;
import dr.util.*;
import org.w3c.dom.*;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.*;
import java.util.*;
public class XMLParser {
public static final String ID = XMLObject.ID;
public static final String IDREF = "idref";
public static final String CONCURRENT = "concurrent";
private Vector<Thread> threads = new Vector<Thread>();
protected boolean strictXML;
protected boolean parserWarnings;
public XMLParser(boolean parserWarnings, boolean strictXML) {
this.parserWarnings = parserWarnings;
this.strictXML = strictXML;
addXMLObjectParser(new ArrayParser(), false);
addXMLObjectParser(Report.PARSER, false);
}
public XMLParser(boolean verbose, boolean parserWarnings, boolean strictXML) {
this(parserWarnings, strictXML);
this.verbose = verbose;
}
public void addXMLObjectParser(XMLObjectParser parser) {
addXMLObjectParser(parser, false);
}
public boolean addXMLObjectParser(XMLObjectParser parser, boolean canReplace) {
boolean replaced = false;
String[] parserNames = parser.getParserNames();
for (String parserName : parserNames) {
XMLObjectParser oldParser = parserStore.get(parserName);
if (oldParser != null) {
if (!canReplace) {
throw new IllegalArgumentException("New parser (" + parser.getParserName()
+ ") in {" + parser.getReturnType() + "} cannot replace existing parser ("
+ oldParser.getParserName() + ") in {" + oldParser.getReturnType() + "}");
} else {
replaced = true;
}
}
parserStore.put(parserName, parser);
}
return replaced;
}
public Iterator getParserNames() {
return parserStore.keySet().iterator();
}
public XMLObjectParser getParser(String name) {
return parserStore.get(name);
}
public Iterator getParsers() {
return parserStore.values().iterator();
}
public Iterator getThreads() {
return threads.iterator();
}
public void storeObject(String name, Object object) {
XMLObject xo = new XMLObject(null, null /*, objectStore*/);
xo.setNativeObject(object);
objectStore.put(name, xo);
}
/**
* An alternative parser that parses until it finds an object of the given
* class and then returns it.
*
* @param reader the reader
* @param target the target class
* @return
* @throws java.io.IOException
* @throws org.xml.sax.SAXException
* @throws dr.xml.XMLParseException
* @throws javax.xml.parsers.ParserConfigurationException
*
*/
public Object parse(Reader reader, Class target)
throws java.io.IOException,
org.xml.sax.SAXException,
dr.xml.XMLParseException,
javax.xml.parsers.ParserConfigurationException {
InputSource in = new InputSource(reader);
javax.xml.parsers.DocumentBuilderFactory documentBuilderFactory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
javax.xml.parsers.DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(in);
Element e = document.getDocumentElement();
if (e.getTagName().equals("beast")) {
concurrent = false;
return convert(e, target, null, false, true);
} else {
throw new dr.xml.XMLParseException("Unknown root document element, " + e.getTagName());
}
}
public Map<String, XMLObject> parse(Reader reader, boolean run)
throws java.io.IOException,
org.xml.sax.SAXException,
dr.xml.XMLParseException,
javax.xml.parsers.ParserConfigurationException {
InputSource in = new InputSource(reader);
javax.xml.parsers.DocumentBuilderFactory documentBuilderFactory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
javax.xml.parsers.DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
documentBuilder.setErrorHandler(new MyErrorHandler());
Document document = documentBuilder.parse(in);
Element e = document.getDocumentElement();
if (e.getTagName().equals("beast")) {
concurrent = false;
root = (XMLObject) convert(e, null, null, run, true);
} else {
throw new dr.xml.XMLParseException("Unknown root document element, " + e.getTagName());
}
return objectStore;
}
private class MyErrorHandler extends DefaultHandler {
public void warning(SAXParseException e) throws SAXException {
System.out.println("Warning: ");
printInfo(e);
}
public void error(SAXParseException e) throws SAXException {
System.out.println("Error: ");
printInfo(e);
}
public void fatalError(SAXParseException e) throws SAXException {
System.out.println("Fatal error: ");
printInfo(e);
}
private void printInfo(SAXParseException e) {
// System.out.println("\tPublic ID: " + e.getPublicId());
// System.out.println("\tSystem ID: " + e.getSystemId());
System.out.println("\tLine number: " + e.getLineNumber());
System.out.println("\tColumn number: " + e.getColumnNumber());
System.out.println("\tError message: " + e.getMessage());
}
}
public XMLObject getRoot() {
return root;
}
private Object convert(Element e, Class target, XMLObject parent, boolean run, boolean doParse) throws XMLParseException {
int index = -1;
if (e.hasAttribute(IDREF)) {
String idref = e.getAttribute(IDREF);
if (e.hasAttribute("index")) {
index = Integer.parseInt(e.getAttribute("index"));
}
if ((e.getAttributes().getLength() > 1 || e.getChildNodes().getLength() > 1) && index == -1) {
throw new XMLParseException("Object with idref=" + idref + " must not have other content or attributes (or perhaps it was not intended to be a reference?).");
}
XMLObject restoredXMLObject = objectStore.get(idref);
if (index != -1) {
if (restoredXMLObject.getNativeObject() instanceof List) {
restoredXMLObject = new XMLObject(restoredXMLObject, index);
} else {
throw new XMLParseException("Trying to get indexed object from non-list");
}
}
if (restoredXMLObject == null) {
throw new XMLParseException("Object with idref=" + idref + " has not been previously declared.");
}
if (restoredXMLObject.getNativeObject() == null) {
throw new XMLParseException("Object with idref=" + idref + " has not been parsed.");
}
XMLObjectParser parser = parserStore.get(e.getTagName());
boolean classMatch = parser != null && parser.getReturnType().isAssignableFrom(restoredXMLObject.getNativeObject().getClass());
if (!e.getTagName().equals(restoredXMLObject.getName()) && !classMatch) {
String msg = "Element named " + e.getTagName() + " with idref=" + idref +
" does not match stored object with same id and tag name " + restoredXMLObject.getName();
if (strictXML) {
throw new XMLParseException(msg);
} else if (parserWarnings) {
// System.err.println("WARNING: " + msg);
java.util.logging.Logger.getLogger("dr.xml").warning(msg);
}
}
if (verbose) System.out.println(" Restoring idref=" + idref);
return new Reference(restoredXMLObject);
} else {
int repeats = 1;
if (e.getTagName().equals(CONCURRENT)) {
if (concurrent) throw new XMLParseException("Nested concurrent elements not allowed.");
concurrent = true;
threads = new Vector<Thread>();
} else if (e.getTagName().equals("repeat")) {
repeats = Integer.parseInt(e.getAttribute("count"));
}
XMLObject xo = new XMLObject(e, parent);
final XMLObjectParser parser = doParse ? parserStore.get(xo.getName()) : null;
String id = null;
NodeList nodes = e.getChildNodes();
for (int k = 0; k < repeats; k++) {
for (int i = 0; i < nodes.getLength(); i++) {
Node child = nodes.item(i);
if (child instanceof Element) {
final Element element = (Element) child;
final String tag = element.getTagName();
if (verbose) System.out.println("Parsing " + tag);
// don't parse elements that may be legal here with global parsers
final boolean parseIt = parser == null || !parser.isAllowed(tag);
Object xoc = convert(element, target, xo, run, parseIt);
xo.addChild(xoc);
if (target != null && xoc instanceof XMLObject) {
Object obj = ((XMLObject) xoc).getNativeObject();
if (obj != null && target.isInstance(obj)) {
return obj;
}
}
} else if (child instanceof Text) {
// just add text as a child of type String object
String text = ((Text) child).getData().trim();
if (text.length() > 0) {
xo.addChild(text);
}
}
}
}
if (e.hasAttribute(ID)) {
id = e.getAttribute(ID);
}
if ((id != null) && objectStore.get(id) != null) {
throw new XMLParseException("Object with Id=" + id + " already exists");
}
Object obj = null;
if (parser != null) {
obj = parser.parseXMLObject(xo, id, objectStore, strictXML);
if (obj instanceof Identifiable) {
((Identifiable) obj).setId(id);
}
if (obj instanceof Citable) {
addCitable((Citable)obj);
}
if (obj instanceof Likelihood) {
Likelihood.FULL_LIKELIHOOD_SET.add((Likelihood) obj);
} else if (obj instanceof Model) {
Model.FULL_MODEL_SET.add((Model) obj);
} else if (obj instanceof Parameter) {
Parameter.FULL_PARAMETER_SET.add((Parameter) obj);
}
xo.setNativeObject(obj);
}
if (id != null) {
if (verbose) System.out.println(" Storing " + xo.getName() + " with id=" + id);
objectStore.put(id, xo);
}
if (run) {
if (e.getTagName().equals(CONCURRENT)) {
for (int i = 0; i < xo.getChildCount(); i++) {
Object child = xo.getChild(i);
if (child instanceof Runnable) {
Thread thread = new Thread((Runnable) child);
thread.start();
threads.add(thread);
} else throw new XMLParseException("Concurrent element children must be runnable!");
}
concurrent = false;
// wait for all threads collected to die
for (Object thread1 : threads) {
waitForThread((Thread) thread1);
}
} else if (obj instanceof Runnable && !concurrent) {
executingRunnable();
if (obj instanceof Spawnable && !((Spawnable) obj).getSpawnable()) {
((Spawnable) obj).run();
} else {
Thread thread = new Thread((Runnable) obj);
thread.start();
threads.add(thread);
waitForThread(thread);
}
}
threads.removeAllElements();
}
return xo;
}
}
protected void executingRunnable() {
// do nothing - for overriding by subclasses
}
public Map<Pair<String, String>, List<Citation>> getCitationStore() {
return citationStore;
}
public static FileReader getFileReader(XMLObject xo, String attributeName) throws XMLParseException {
if (xo.hasAttribute(attributeName)) {
final File inFile = getFileHandle(xo, attributeName);
try {
return new FileReader(inFile);
} catch (FileNotFoundException e) {
throw new XMLParseException("Input file " + inFile.getName()
+ " was not found in the working directory");
}
}
throw new XMLParseException("Error reading input file in " + xo.getId());
}
/**
* Get filename and path from BEAST XML object
*
* @param xo
* @return
*/
private static File getFileHandle(XMLObject xo, String attributeName) throws XMLParseException {
String fileName = xo.getStringAttribute(attributeName);
// Check to see if a filename prefix has been specified, check it doesn't contain directory
// separator characters and then prefix it.
final String fileNamePrefix = System.getProperty("file.name.prefix");
final String fileSeparator = System.getProperty("file.separator");
if (fileNamePrefix != null) {
if (fileNamePrefix.trim().length() == 0 || fileNamePrefix.contains(fileSeparator)) {
throw new XMLParseException("The specified file name prefix is illegal.");
}
}
final String fileRankPostfix = System.getProperty("mpi.rank.postfix");
if (fileRankPostfix != null) {
if (fileName.endsWith(".log")) {
fileName = fileName.substring(0, fileName.length() - 4) + fileRankPostfix + ".log";
}
if (fileName.endsWith(".trees")) {
fileName = fileName.substring(0, fileName.length() - 6) + fileRankPostfix + ".trees";
}
}
return FileHelpers.getFile(fileName, fileNamePrefix);
}
/**
* Allow a file relative to beast xml file with a prefix of ./
*
* @param xo element
* @param parserName for error messages
* @return Print writer from fileName attribute in the given XMLObject
* @throws XMLParseException if file can't be created for some reason
*/
public static PrintWriter getFilePrintWriter(XMLObject xo, String parserName) throws XMLParseException {
return getFilePrintWriter(xo, parserName, FileHelpers.FILE_NAME);
}
public static PrintWriter getFilePrintWriter(XMLObject xo, String parserName, String attributeName) throws XMLParseException {
if (xo.hasAttribute(attributeName)) {
File logFile = getLogFile(xo, attributeName);
try {
return new PrintWriter(new FileOutputStream(logFile));
} catch (FileNotFoundException fnfe) {
throw new XMLParseException("File '" + logFile.getAbsolutePath() +
"' can not be opened for " + parserName + " element.");
}
}
return new PrintWriter(System.out);
}
public static File getLogFile(XMLObject xo, String attributeName) throws XMLParseException {
final File logFile = getFileHandle(xo, attributeName);
boolean allowOverwrite = false;
if (xo.hasAttribute(LoggerParser.ALLOW_OVERWRITE_LOG)) {
allowOverwrite = xo.getBooleanAttribute(LoggerParser.ALLOW_OVERWRITE_LOG);
}
// override with a runtime set System Property
if (System.getProperty("log.allow.overwrite") != null) {
allowOverwrite = Boolean.parseBoolean(System.getProperty("log.allow.overwrite", "false"));
}
if (logFile.exists() && !allowOverwrite) {
throw new XMLParseException("\nThe log file " + logFile.getName() + " already exists in the working directory." +
"\nTo allow it to be overwritten, use the '-overwrite' command line option when running" +
"\nBEAST or select the option in the Run Options dialog box as appropriate.");
}
return logFile;
}
public Map<String, XMLObject> getObjectStore() {
return objectStore;
}
public class ArrayParser extends AbstractXMLObjectParser {
public String getParserName() {
return "array";
}
public Object parseXMLObject(XMLObject xo) throws XMLParseException {
List<Object> list = new ArrayList<Object>();
for (int i = 0; i < xo.getChildCount(); i++) {
list.add(xo.getChild(i));
}
return list;
}
//************************************************************************
// AbstractXMLObjectParser implementation
//************************************************************************
public String getParserDescription() {
return "This element returns an array of the objects it contains.";
}
public Class getReturnType() {
return Object[].class;
}
public XMLSyntaxRule[] getSyntaxRules() {
return new XMLSyntaxRule[]{new ElementRule(Object.class, "Objects to be put in an array", 1, Integer.MAX_VALUE)};
}
}
private void waitForThread(Thread thread) {
// wait doggedly for thread to die
while (thread.isAlive()) {
try {
thread.join();
} catch (InterruptedException ie) {
// DO NOTHING
}
}
}
// //anonymous object store class
// private final ObjectStore objectStore = new ObjectStore() {
// public Object getObjectById(Object uid) throws ObjectNotFoundException {
// XMLObject obj = (XMLObject) store.get(uid);
// if (obj == null) throw new ObjectNotFoundException("Object with uid=" + uid + " not found in ObjectStore");
// if (obj.hasNativeObject()) return obj.getNativeObject();
// return obj;
// }
//
// public boolean hasObjectId(Object uid) {
// Object obj = store.get(uid);
// return (obj != null);
// }
//
// public Set getIdSet() {
// return store.keySet();
// }
//
// public Collection getObjects() {
// return store.values();
// }
//
//// public void addIdentifiableObject(Identifiable obj, boolean force) {
////
//// String id = obj.getId();
//// if (id == null) throw new IllegalArgumentException("Object must have a non-null identifier.");
////
//// if (force) {
//// store.put(id, obj);
//// } else {
//// if (store.get(id) == null) {
//// store.put(id, obj);
//// }
//// }
//// }
// };
public void addCitable(Citable citable) {
// remove 'In prep' citations
List<Citation> citationList = new LinkedList<Citation>();
for (Citation citation : citable.getCitations()) {
if (citation.getStatus() != Citation.Status.IN_PREPARATION) {
citationList.add(citation);
}
}
if (citationList.size() > 0) {
Pair<String, String> pair = new Pair<String, String>(citable.getCategory().toString(),
citable.getDescription());
citationStore.put(pair, citationList);
}
}
// private final Hashtable<String, XMLObject> store = new Hashtable<String, XMLObject>();
private final Map<String, XMLObjectParser> parserStore = new TreeMap<String, XMLObjectParser>(new ParserComparator());
private final Map<String, XMLObject> objectStore = new LinkedHashMap<String, XMLObject>();
private final Map<Pair<String, String>, List<Citation>> citationStore = new LinkedHashMap<Pair<String, String>, List<Citation>>();
private boolean concurrent = false;
private XMLObject root = null;
private boolean verbose = false;
public static class Utils {
/**
* Throws a runtime exception if the element does not have
* the given name.
*/
public static void validateTagName(Element e, String name) throws XMLParseException {
if (!e.getTagName().equals(name)) {
throw new XMLParseException("Wrong tag name! Expected " + name + ", found " + e.getTagName() + ".");
}
}
public static boolean hasAttribute(Element e, String name) {
String attr = e.getAttribute(name);
return ((attr != null) && !attr.equals(""));
}
/**
* @return the first child element of the given name.
*/
public static Element getFirstByName(Element parent, String name) {
NodeList nodes = parent.getElementsByTagName(name);
if (nodes.getLength() > 0) {
return (Element) nodes.item(0);
} else return null;
}
}
class ParserComparator implements Comparator<String> {
public int compare(String o1, String o2) {
String name1 = o1.toUpperCase();
String name2 = o2.toUpperCase();
return name1.compareTo(name2);
}
}
}