/*
* File JSONParser.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 java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
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 org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import beast.app.beauti.PartitionContext;
import beast.core.BEASTInterface;
import beast.core.Input;
import beast.core.Input.Validate;
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.util.XMLParser.NameValuePair;
/** parses BEAST JSON file into a set of BEAST objects **/
public class JSONParser {
final static String INPUT_CLASS = Input.class.getName();
final static String BEAST_OBJECT_CLASS = BEASTInterface.class.getName();
final static String RUNNABLE_CLASS = Runnable.class.getName();
Runnable runnable;
State state;
/**
* JSONObject document representation of JSON file *
*/
JSONObject doc;
/**
* maps sequence data onto integer value *
*/
String DataMap;
HashMap<String, BEASTInterface> IDMap;
HashMap<String, Integer[]> likelihoodMap;
HashMap<String, JSONObject> IDNodeMap;
static HashMap<String, String> element2ClassMap;
static Set<String> reservedElements;
static {
element2ClassMap = new HashMap<>();
reservedElements = new HashSet<>();
for (String element : element2ClassMap.keySet()) {
reservedElements.add(element);
}
}
class BEASTObjectWrapper {
public BEASTObjectWrapper(BEASTInterface object, JSONObject node) {
this.object = object;
this.node = node;
}
BEASTInterface object;
JSONObject node;
}
List<BEASTObjectWrapper> objectsWaitingToInit;
public HashMap<String, String> getElement2ClassMap() {
return element2ClassMap;
}
String[] nameSpaces;
/**
* Flag to indicate initAndValidate should be called after all inputs of a
* beastObject have been parsed
*/
boolean initialise = true;
/**
* when parsing JSON, missing inputs can be assigned default values through a
* RequiredInputProvider
*/
RequiredInputProvider requiredInputProvider = null;
PartitionContext partitionContext = null;
public JSONParser() {
objectsWaitingToInit = new ArrayList<>();
}
public Runnable parseFile(File file) throws IOException, JSONException, JSONParserException {
// parse the JSON file into a JSONObject
// first get rid of comments: remove all text on lines starting with space followed by //
// keep line breaks so that error reporting indicates the correct line.
BufferedReader fin = new BufferedReader(new FileReader(file));
StringBuffer buf = new StringBuffer();
String str = null;
while (fin.ready()) {
str = fin.readLine();
if (!str.matches("^\\s*//.*")) {
buf.append(str);
}
buf.append('\n');
}
fin.close();
doc = new JSONObject(buf.toString());
processPlates(doc);
int pointIdx = file.getName().lastIndexOf('.');
String baseName = pointIdx<0 ? file.getName() : file.getName().substring(0, pointIdx);
replaceVariable(doc, "filebase", baseName);
// Substitute occurrences of "$(seed)" with RNG seed
replaceVariable(doc, "seed", String.valueOf(Randomizer.getSeed()));
IDMap = new HashMap<>();
likelihoodMap = new HashMap<>();
IDNodeMap = new HashMap<>();
parse();
// assert m_runnable == null || m_runnable instanceof Runnable;
if (runnable != null)
return runnable;
else {
throw new IOException("Run element does not point to a runnable object.");
}
} // parseFile
/**
* @param node the node to do variable replacement in
* @param var the variable name to replace
* @param value the value to replace the variable name with
*/
public static void replaceVariable(final Object json, final String var, final String value) {
try {
if (json instanceof JSONObject) {
final JSONObject jsonobject = (JSONObject) json;
for (String key : jsonobject.keySet()) {
final Object attr = jsonobject.get(key);
if (attr instanceof String) {
if (((String) attr).contains("$(" + var + ")")) {
String att = (String) attr;
att = att.replaceAll("\\$\\(" + var + "\\)", value);
jsonobject.put(key, att);
}
} else if (attr instanceof JSONObject) {
replaceVariable(attr, var, value);
} else if (attr instanceof JSONArray) {
replaceVariable(attr, var, value);
}
}
} else if (json instanceof JSONArray) {
JSONArray array = (JSONArray) json;
for (int i = 0; i < array.length(); i++) {
Object o2 = array.get(i);
replaceVariable(o2, var, value);
}
} else {
// ignore
}
} catch (JSONException e) {
// ignore?
}
} // replaceVariable
/**
* extract all elements (runnable or not) from an XML fragment. Useful for
* retrieving all non-runnable elements when a template is instantiated by
* Beauti *
*/
// public List<BEASTObject> parseTemplate(String xml, HashMap<String, BEASTObject>
// idMap, boolean initialise) {
// m_bInitialize = initialise;
// // parse the XML file into a DOM document
// DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// //factory.setValidating(true);
// doc = factory.newDocumentBuilder().parse(new InputSource(new
// StringReader(xml)));
// processPlates();
//
// IDMap = idMap;//new HashMap<>();
// likelihoodMap = new HashMap<>();
// IDNodeMap = new HashMap<>();
//
// List<BEASTObject> beastObjects = new ArrayList<>();
//
// // find top level beast element
// NodeList nodes = doc.getElementsByTagName("*");
// if (nodes == null || nodes.getLength() == 0) {
// throw new Exception("Expected top level beast element in XML");
// }
// Node topNode = nodes.item(0);
// // sanity check that we are reading a beast 2 file
// 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;
// // }
//
//
// initIDNodeMap(topNode);
// parseNameSpaceAndMap(topNode);
//
// NodeList children = topNode.getChildNodes();
// for (int i = 0; i < children.getLength(); i++) {
// if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
// Node child = children.item(i);
// Log.warning.println(child.getNodeName());
// if (!child.getNodeName().equals(MAP_ELEMENT)) {
// beastObjects.add(createObject(child, PLUGIN_CLASS, null));
// }
// }
// }
// initPlugins();
// return beastObjects;
// } // parseTemplate
private void initBEASTObjects() throws JSONParserException {
JSONObject node = null;
try {
for (int i = 0; i < objectsWaitingToInit.size(); i++) {
BEASTObjectWrapper bow = objectsWaitingToInit.get(i);
// for error handling, init node
node = bow.node;
bow.object.initAndValidate();
}
} catch (Exception e) {
// next lines for debugging only
// beastObject.validateInputs();
// beastObject.initAndValidate();
e.printStackTrace();
throw new JSONParserException(node, "validate and intialize error: " + e.getMessage(), 110);
}
}
/**
* Expand plates in JSON by duplicating the containing JSON and replacing the
* plate variable with the appropriate value.
* "plate":{"var":"n",
* "range": ["CO1", "CO2", "Nuc"],
* "content": [
* {"part":"$(n)"}
* {"otherpart":"$(n).$(m)"}
* {"yetotherpart":"xyz$(n)"}
* ]
* }
*
* is replaced by
*
* {"part":"CO1"}
* {"otherpart":"CO1.$(m)"}
* {"yetotherpart":"xyzCO1"}
* {"part":"CO2"}
* {"otherpart":"CO2.$(m)"}
* {"yetotherpart":"xyzCO2"}
* {"part":"Nuc"}
* {"otherpart":"Nuc.$(m)"}
* {"yetotherpart":"xyzNuc"}
*
*/
void processPlates(JSONObject node) throws IOException, JSONException, JSONParserException {
for (String key : node.keySet()) {
Object o = node.get(key);
if (o instanceof JSONObject) {
JSONObject child = (JSONObject) o;
processPlates(child);
}
if (o instanceof JSONArray) {
JSONArray list = (JSONArray) o;
for (int i = 0; i < list.length(); i++) {
Object o2 = list.get(i);
if (o2 instanceof JSONObject) {
JSONObject child = (JSONObject) o2;
processPlates(child);
if (child.has("plate")) {
unrollPlate(list, child);
}
}
}
}
}
} // processPlates
private void unrollPlate(JSONArray list, JSONObject plate) throws IOException, JSONParserException, JSONException {
int index = list.indexOf(plate);
if (index < 0) {
throw new RuntimeException("Programmer error: plate should be in list");
}
list.remove(index);
if (plate.keySet().size() != 3 ||
!plate.has("plate") ||
!plate.has("range") ||
!plate.has("var")) {
throw new JSONParserException(plate, "Plate should only have tree attributes: plate, range and var", 1007);
}
Object o = plate.get("range");
if (!(o instanceof JSONArray)) {
throw new JSONParserException(plate, "Plate attribute range should be a list", 1008);
}
JSONArray range = (JSONArray) o;
o = plate.get("var");
if (!(o instanceof String)) {
throw new JSONParserException(plate, "Plate attribute var should be a string", 1009);
}
String varStr = (String) o;
for (int i = 0; i < range.length(); i++) {
o = range.get(i);
if (!(o instanceof String)) {
throw new JSONParserException(plate, "Plate range value should be a string", 1010);
}
String valueStr = (String) o;
Object copy = copyReplace(plate, varStr, valueStr);
list.insert(index + i, copy);
}
} // unrollPlate
private Object copyReplace(Object o, String varStr, String valueStr) {
if (o instanceof Number) {
return o;
} else if (o instanceof Boolean) {
return o;
} else if (o instanceof String) {
String str = (String) o;
str = str.replaceAll("\\$\\(" + varStr + "\\)", valueStr);
return str;
} else if (o instanceof JSONObject) {
JSONObject orig = (JSONObject) o;
JSONObject copy = new JSONObject();
for (String key : orig.keySet()) {
try {
Object value = orig.get(key);
Object copyValue = copyReplace(value, varStr, valueStr);
copy.put(key, copyValue);
} catch (JSONException e) {
// T O D O Auto-generated catch block
e.printStackTrace();
}
}
return copy;
} else if (o instanceof JSONArray) {
JSONArray orig = (JSONArray) o;
JSONArray copy = new JSONArray();
for (int i = 0; i < orig.length(); i++) {
Object value;
try {
value = orig.get(i);
Object copyValue = copyReplace(value, varStr, valueStr);
copy.add(copyValue);
} catch (JSONException e) {
// T O D O Auto-generated catch block
e.printStackTrace();
}
}
return copy;
}
throw new RuntimeException("How did we get here?");
} // unrollPlate
/**
* Parse an JSON fragment representing a list of BEASTObjects
*/
public List<Object> parseFragment(final String json, final boolean initialise) throws JSONParserException, JSONException {
this.initialise = initialise;
doc = new JSONObject(json);
// find top level beast element
JSONObject nodes = doc;
if (nodes == null || nodes.keySet().size() == 0) {
throw new JSONParserException(doc, "Expected top level 'beast' element in JSON fragment", 1001);
}
double version = getAttributeAsDouble(nodes, "version");
if (version < 2.0 || version == Double.MAX_VALUE) {
throw new JSONParserException(nodes, "Wrong version: only versions > 2.0 are supported", 101);
}
initIDNodeMap(doc);
parseNameSpaceAndMap(doc);
List<Object> objects = new ArrayList<>();
try {
// find beast element
Object o = doc.get(XMLParser.BEAST_ELEMENT);
if (o == null) {
throw new JSONParserException(nodes, "Expected " + XMLParser.BEAST_ELEMENT + " top level object in file", 102);
}
if (!(o instanceof JSONArray)) {
throw new JSONParserException(nodes, "Expected " + XMLParser.BEAST_ELEMENT + " to be a list", 1020);
}
JSONArray analysis = (JSONArray) o;
for (int i = 0; i < analysis.length(); i++) {
o = analysis.get(i);
if (!(o instanceof JSONObject)) {
throw new JSONParserException(nodes, XMLParser.BEAST_ELEMENT + " should only contain objects", 1021);
}
JSONObject node = (JSONObject) o;
o = createObject(node, Object.class.getName());
objects.add(o);
}
} catch (JSONException e) {
throw new JSONParserException(nodes, e.getMessage(), 1004);
}
if (initialise) {
initBEASTObjects();
}
return objects;
} // parseFragment
//
// /**
// * Parse XML fragment that will be wrapped in a beast element
// * before parsing. This allows for ease of creating BEASTObject 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 BEASTObject parseBareFragment(String xml, boolean initialise) throws
// Exception {
// // get rid of XML processing instruction
// xml = xml.replaceAll("<\\?xml[^>]*>", "");
// if (xml.indexOf("<beast") > -1) {
// return parseFragment(xml, initialise);
// } else {
// return parseFragment("<beast>" + xml + "</beast>", initialise);
// }
// }
//
// public List<BEASTObject> parseBareFragments(String xml, boolean initialise)
//{
// m_bInitialize = initialise;
// // parse the XML fragment into a DOM document
// DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// doc = factory.newDocumentBuilder().parse(new InputSource(new
// StringReader(xml)));
// doc.normalize();
// processPlates();
//
// // find top level beast element
// NodeList nodes = doc.getElementsByTagName("*");
// if (nodes == null || nodes.getLength() == 0) {
// throw new Exception("Expected top level beast element in XML");
// }
// Node topNode = nodes.item(0);
// initIDNodeMap(topNode);
// parseNameSpaceAndMap(topNode);
//
// NodeList children = topNode.getChildNodes();
// List<BEASTObject> beastObjects = new ArrayList<>();
// for (int i = 0; i < children.getLength(); i++) {
// if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
// BEASTObject beastObject = createObject(children.item(i), PLUGIN_CLASS, null);
// beastObjects.add(beastObject);
// }
// }
// initPlugins();
// return beastObjects;
// }
/**
* parse BEAST file as DOM document
*
* @throws JSONParserException
*/
public void parse() throws JSONParserException {
// find top level beast element
JSONObject nodes = doc;
if (nodes == null || nodes.keySet().size() == 0) {
throw new JSONParserException(doc, "Expected top level beast element in JSON", 1001);
}
double version = getAttributeAsDouble(nodes, "version");
if (version < 2.0 || version == Double.MAX_VALUE) {
throw new JSONParserException(nodes, "Wrong version: only versions > 2.0 are supported", 101);
}
initIDNodeMap(doc);
parseNameSpaceAndMap(doc);
// parseState();
parseRunElement(doc);
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 JSONParserException
*/
void initIDNodeMap(JSONObject node) throws JSONParserException {
String ID = getID(node);
if (ID != null) {
if (IDNodeMap.containsKey(ID)) {
throw new JSONParserException(node, "IDs should be unique. Duplicate id '" + ID + "' found", 104);
}
IDNodeMap.put(ID, node);
}
for (Object key : node.keySet()) {
try {
Object o = node.get((String) key);
if (o instanceof JSONObject) {
initIDNodeMap((JSONObject) o);
}
if (o instanceof JSONArray) {
JSONArray list = (JSONArray) o;
for (int i = 0; i < list.length(); i++) {
Object o2 = list.get(i);
if (o2 instanceof JSONObject) {
initIDNodeMap((JSONObject) o2);
}
}
}
} catch (JSONException e) {
throw new JSONParserException(node, e.getMessage(), 1002);
}
}
}
/**
* 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 JSONParserException
*/
void parseNameSpaceAndMap(JSONObject topNode) throws JSONParserException {
// process namespaces
if (topNode.has("namespace")) {
String nameSpace = getAttribute(topNode, "namespace");
setNameSpace(nameSpace);
} else {
// make sure that the default namespace is in there
if (nameSpaces == null) {
nameSpaces = new String[1];
nameSpaces[0] = "";
}
}
// process map elements
if (topNode.has("map")) {
try {
Object o = topNode.get("map");
if (o instanceof JSONArray) {
JSONArray maps = (JSONArray) o;
for (int i = 0; i < maps.length(); i++) {
Object o2 = maps.get(i);
if (o2 instanceof JSONObject) {
JSONObject map = (JSONObject) o2;
if (map.has("name") && map.has("value") && map.length()==2) {
String mapName = map.getString("name");
String clazz = map.getString("value");
// remove spaces
clazz = clazz.replaceAll("\\s", "");
// go through namespaces in order they are declared to find the
// correct class
boolean done = false;
for (String nameSpace : nameSpaces) {
// sanity check: class should exist
try {
if (!done && Class.forName(nameSpace + clazz) != null) {
element2ClassMap.put(mapName, clazz);
Log.warning.println(mapName + " => " + nameSpace + clazz);
done = true;
//String reserved = getAttribute(child, "reserved");
//if (reserved != null && reserved.toLowerCase().equals("true")) {
// reservedElements.add(name);
//}
}
} catch (ClassNotFoundException e) {
// ignore -- it may be in another namespace
// there appears to be no good way to check a class exists other than to try and create one
// and test whether no exception is thrown.
}
}
if (!done) {
Log.warning.println("WARNING: no class could be found for map " + mapName + " => " + clazz +". This map is ignored.");
}
} else {
throw new JSONParserException(map, "Expected a name and a value and nothing else", 1016);
}
} else {
throw new JSONParserException(topNode, "map should be a list of JSONObjects. Use for example map:[{name:\"OneOnX\", value:\"beast.math.distributions.OneOnX\"}] for a single map", 1013);
}
}
} else {
throw new JSONParserException(topNode, "map should be a list. Use for example map:[{name:\"OneOnX\", value:\"beast.math.distributions.OneOnX\"}] for a single map", 1014);
}
} catch (JSONException e) {
// should never get here, unless something is really wrong
e.printStackTrace();
throw new RuntimeException(e);
}
}
} // parseNameSpaceAndMap
public void setNameSpace(String nameSpaceStr) {
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) {
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(JSONObject topNode) throws JSONParserException {
// find beast element
try {
Object o = doc.get(XMLParser.BEAST_ELEMENT);
if (o == null) {
throw new JSONParserException(topNode, "Expected " + XMLParser.BEAST_ELEMENT + " top level object in file", 102);
}
if (!(o instanceof JSONArray)) {
throw new JSONParserException(topNode, "Expected " + XMLParser.BEAST_ELEMENT + " to be a list", 1020);
}
JSONArray analysis = (JSONArray) o;
runnable = null;
for (int i = 0; i < analysis.length(); i++) {
o = analysis.get(i);
if (!(o instanceof JSONObject)) {
throw new JSONParserException(topNode, XMLParser.BEAST_ELEMENT + " should only contain objects", 1021);
}
JSONObject node = (JSONObject) o;
o = createObject(node, RUNNABLE_CLASS);
if (o instanceof Runnable) {
if (runnable != null) {
throw new JSONParserException(node, "Expected only one runnable element in file", 103);
}
runnable = (Runnable) o;
}
}
if (runnable == null) {
throw new JSONParserException(topNode, "Expected at least one runnable element in file", 1030);
}
} catch (JSONException e) {
throw new JSONParserException(topNode, e.getMessage(), 1004);
}
} // parseRunElement
/**
* Check that BEASTObject 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...
*/
boolean checkType(String className, BEASTInterface beastObject) throws JSONParserException {
// parameter clutch
if (beastObject instanceof Parameter<?>) {
for (String nameSpace : nameSpaces) {
if ((nameSpace + className).equals(RealParameter.class.getName())) {
return true;
}
}
}
if (className.equals(INPUT_CLASS)) {
return true;
}
for (String nameSpace : nameSpaces) {
try {
if (Class.forName(nameSpace + className).isInstance(beastObject)) {
return true;
}
} catch (Exception e) {
// ignore
}
}
return false;
} // checkType
/** create a BEAST object based on the info in the node.
* @param node
* @param className default class name -- this means a spec attribute does not need to be
* specified if all outputs of this BEAST object have as Input type the class of this object.
* @return
* @throws JSONParserException
*/
BEASTInterface createObject(JSONObject node, String className) throws JSONParserException {
// try the IDMap first
String ID = getID(node);
if (ID != null) {
if (IDMap.containsKey(ID)) {
BEASTInterface beastObject = IDMap.get(ID);
if (checkType(className, beastObject)) {
return beastObject;
}
throw new JSONParserException(node, "id=" + ID + ". Expected object of type " + className + " instead of "
+ beastObject.getClass().getName(), 105);
}
}
String IDRef = getIDRef(node);
if (IDRef != null) {
// produce warning if there are other attributes than idref
if (node.keySet().size() > 1) {
// check if there is just 1 attribute
Log.warning.println("Element " + getAttribute((JSONObject) node.getParent(), "name") + " found with idref='" + IDRef
+ "'. All other attributes are ignored.\n");
}
if (IDMap.containsKey(IDRef)) {
BEASTInterface beastObject = IDMap.get(IDRef);
if (checkType(className, beastObject)) {
return beastObject;
}
throw new JSONParserException(node, "id=" + IDRef + ". Expected object of type " + className + " instead of "
+ beastObject.getClass().getName(), 106);
} else if (IDNodeMap.containsKey(IDRef)) {
BEASTInterface beastObject = createObject(IDNodeMap.get(IDRef), className);
if (checkType(className, beastObject)) {
return beastObject;
}
throw new JSONParserException(node, "id=" + IDRef + ". Expected object of type " + className + " instead of "
+ beastObject.getClass().getName(), 107);
}
throw new JSONParserException(node, "Could not find object associated with idref " + IDRef, 170);
}
// it's not in the ID map yet, so we have to create a new object
String specClass = className;
String elementName = getElementName(node);
if (element2ClassMap.containsKey(elementName)) {
specClass = element2ClassMap.get(elementName);
}
String spec = getAttribute(node, "spec");
if (spec != null) {
specClass = spec;
}
String clazzName = null;
// determine clazzName from specName, taking name spaces in account
for (String nameSpace : nameSpaces) {
if (clazzName == null) {
if (XMLParserUtils.beastObjectNames.contains(nameSpace + specClass)) {
clazzName = nameSpace + specClass;
break;
}
}
}
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) {
throw new JSONParserException(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)) {
// if (o instanceof Input) {
// // if we got this far, it is a basic input,
// // that is, one of the form <input name='xyz'>value</input>
// String name = getAttribute(node, "name");
// if (name == null) {
// name = "value";
// }
// String text = node.getTextContent();
// if (text.length() > 0) {
// setInput(node, parent, name, text);
// }
// return null;
// } else {
throw new JSONParserException(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 (initialise) {
try {
beastObject.validateInputs();
objectsWaitingToInit.add(new BEASTObjectWrapper(beastObject, node));
// beastObject.initAndValidate();
} catch (IllegalArgumentException e) {
// next lines for debugging only
// beastObject.validateInputs();
// beastObject.initAndValidate();
e.printStackTrace();
throw new JSONParserException(node, "validate and intialize error: " + e.getMessage(), 110);
}
}
return beastObject;
} // createObject
private String getElementName(JSONObject node) {
Object o = node.getParent();
if (o == null) {
return null;
}
if (o instanceof JSONObject) {
JSONObject parent = ((JSONObject) o);
for (String s : parent.keySet()) {
try {
if (parent.get(s) == node) {
return s;
}
} catch (JSONException e) {
// should not get here
e.printStackTrace();
}
}
}
if (o instanceof JSONArray) {
JSONArray parent = ((JSONArray) o);
Object o2 = parent.getParent();
if (o2 == null) {
return null;
}
if (o2 instanceof JSONObject) {
JSONObject gparent = ((JSONObject) o2);
for (String s : gparent.keySet()) {
try {
if (gparent.get(s) == parent) {
return s;
}
} catch (JSONException e) {
// should not get here
e.printStackTrace();
}
}
}
}
return null;
}
/** create BEASTInterface either using Inputs, or using annotated constructor **/
private BEASTInterface createBeastObject(JSONObject node, String ID, String clazzName, List<NameValuePair> inputInfo) throws JSONParserException {
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 JSONParserException(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 JSONParserException(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) {
state = (State) o;
}
// process inputs for annotated constructors
for (NameValuePair pair : inputInfo) {
setInput(node, beastObject, pair.name, pair.value);
}
// 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 JSONParserException(node, e.getMessage(), 1008);
}
// sanity check: all attributes should be valid input names
if (!(beastObject instanceof Map)) {
for (String name : node.keySet()) {
if (!(name.equals("id") || name.equals("idref") || name.equals("spec") || name.equals("name"))) {
try {
beastObject.getInput(name);
} catch (Exception e) {
throw new JSONParserException(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(JSONObject node, String _id, String clazzName, List<NameValuePair> inputInfo) throws JSONParserException {
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) {
// collect Param annotations on constructor parameters
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();
if (types.length > 0 && paramAnnotations.size() == types.length) {
try {
// if all constructor parameters have Param annotations, try to call constructor
// first, build up argument list, then create object
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 = XMLParser.getListOfValues(param, inputInfo);
((List<Object>) args[i]).addAll(values);
} else {
args[i] = getValue(param, (Class<?>) type, inputInfo);
}
}
// ensure all inputs are used
boolean allUsed = true;
for (NameValuePair pair : inputInfo) {
if (!pair.processed) {
allUsed= false;
}
}
// if all inputs are used, call the constructor, otherwise, look for another constructor
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 JSONParserException(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;
}
/** get value from inputInfo, but use default if the Param name cannot be found in inputInfo
* RRB: would like to combine with XMLParser.getValue, but this may get ugly due to incompatible XML/JSON types
*/
private Object getValue(Param param, Class<?> clazz, List<NameValuePair> inputInfo) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
for (NameValuePair pair : inputInfo) {
if (pair.name.equals(param.name())) {
pair.processed = true;
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 = clazz.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 = clazz.getDeclaredConstructor(int.class);
} catch (NumberFormatException e2) {
// could not parse as integer, try double instead
v = Double.parseDouble(value);
ctor = clazz.getDeclaredConstructor(double.class);
}
}
ctor.setAccessible(true);
final Object o = ctor.newInstance(v);
return o;
}
List<NameValuePair> parseInputs(JSONObject node, String className) throws JSONParserException {
List<NameValuePair> inputInfo = new ArrayList<>();
if (node.keySet() != null) {
try {
// parse inputs in occurrence of inputs in the parent object
// this determines the order in which initAndValidate is called
List<InputType> inputs = XMLParserUtils.listInputs(Class.forName(className), null);
Set<String> done = new HashSet<>();
for (InputType input : inputs) {
String name = input.name;
processInput(name, node, inputInfo, inputs);
done.add(name);
}
for (String name : node.keySet()) {
if (!done.contains(name)) {
// this can happen with Maps
processInput(name, node, inputInfo, inputs);
}
}
} catch (JSONParserException e) {
throw e;
} catch (Exception e) {
throw new JSONParserException(node, e.getMessage(), 1005);
}
}
return inputInfo;
} // setInputs
private void processInput(String name, JSONObject node, List<NameValuePair> map, List<InputType> inputs) throws JSONParserException, JSONException {
if (node.has(name)) {
if (!(name.equals("id") || name.equals("idref") || name.equals("spec") || name.equals("name"))) {
Object o = node.get(name);
if (o instanceof String) {
String value = (String) o;
if (value.startsWith("@")) {
String IDRef = value.substring(1);
JSONObject element = new JSONObject();
element.put("idref", IDRef);
BEASTInterface beastObject = createObject(element, BEAST_OBJECT_CLASS);
map.add(new NameValuePair(name, beastObject));
//setInput(node, parent, name, beastObject);
} else {
map.add(new NameValuePair(name, value));
//setInput(node, parent, name, value);
}
} else if (o instanceof Number) {
map.add(new NameValuePair(name, o));
//parent.setInputValue(name, o);
} else if (o instanceof Boolean) {
map.add(new NameValuePair(name, o));
//parent.setInputValue(name, o);
} else if (o instanceof JSONObject) {
JSONObject child = (JSONObject) o;
String className = getClassName(child, name, inputs);
BEASTInterface childItem = createObject(child, className);
if (childItem != null) {
map.add(new NameValuePair(name, childItem));
//setInput(node, parent, name, childItem);
}
// childElements++;
} else if (o instanceof JSONArray) {
JSONArray list = (JSONArray) o;
for (int i = 0; i < list.length(); i++) {
Object o2 = list.get(i);
if (o2 instanceof JSONObject) {
JSONObject child = (JSONObject) o2;
String className = getClassName(child, name, inputs);
BEASTInterface childItem = createObject(child, className);
if (childItem != null) {
map.add(new NameValuePair(name, childItem));
//setInput(node, parent, name, childItem);
}
} else {
map.add(new NameValuePair(name, o2));
//parent.setInputValue(name, o2);
}
}
} else {
throw new RuntimeException("Developer error: Don't know how to handle this JSON construction");
}
}
}
}
private void setInput(JSONObject node, BEASTInterface beastObject, String name, Object value) throws JSONParserException {
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, value);
} else {
throw new IOException("Multiple entries for non-list input " + input.getName());
}
return;
} 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) {
// T O D O: handle exception
}
throw new JSONParserException(node, e.getMessage() + " expected '" + type + "' but got '"
+ value.getClass().getName().replaceAll(".*\\.", "") + "'", 123);
} else {
throw new JSONParserException(node, e.getMessage(), 130);
}
}
}
/**
* records id in IDMap, for ease of retrieving BeastObjects associated with
* idrefs *
*/
void register(JSONObject node, BEASTInterface beastObject) {
String ID = getID(node);
if (ID != null) {
IDMap.put(ID, beastObject);
}
}
public static String getID(JSONObject node) {
return getAttribute(node, "id");
}
public static String getIDRef(JSONObject node) {
return getAttribute(node, "idref");
}
/**
* get string value of attribute with given name as opposed to double or
* integer value (see methods below) *
*/
public static String getAttribute(JSONObject node, String attName) {
if (node.has(attName)) {
try {
return node.get(attName).toString();
} catch (JSONException e) {
return null;
}
}
return null;
}
/**
* get integer value of attribute with given name *
*/
public static int getAttributeAsInt(JSONObject node, String attName) {
String att = getAttribute(node, attName);
if (att == null) {
return -1;
}
return Integer.parseInt(att);
}
/**
* get double value of attribute with given name
* @throws JSONParserException *
*/
public static double getAttributeAsDouble(JSONObject node, String attName) throws JSONParserException {
String att = getAttribute(node, attName);
if (att == null) {
return Double.MAX_VALUE;
}
try {
return Double.parseDouble(att);
} catch (NumberFormatException e) {
throw new JSONParserException(node, "Could not parse number " + att, 1003);
}
}
public interface RequiredInputProvider {
Object createInput(BEASTInterface beastObject, Input<?> input, PartitionContext context);
}
public void setRequiredInputProvider(RequiredInputProvider provider, PartitionContext context) {
requiredInputProvider = provider;
partitionContext = context;
}
String getClassName(JSONObject child, String name, BEASTInterface parent) {
String className = getAttribute(child, "spec");
if (className == null) {
final Input<?> input = parent.getInput(name);
Class<?> type = input.getType();
if (type == null) {
input.determineClass(parent);
type = input.getType();
}
className = type.getName();
}
if (element2ClassMap.containsKey(className)) {
className = element2ClassMap.get(className);
}
return className;
}
private String getClassName(JSONObject child, String name, List<InputType> inputs) {
String className = getAttribute(child, "spec");
if (className == null) {
// derive type from Input
for (InputType input : inputs) {
if (input.name.equals(name)) {
Class<?> type = input.type;
if (type == null) {
throw new RuntimeException("Programmer error: inputs should have their type set");
}
//if (type.isAssignableFrom(List.class)) {
// System.err.println("XX");
//}
className = type.getName();
}
}
}
if (element2ClassMap.containsKey(className)) {
className = element2ClassMap.get(className);
}
return className;
}
/**
* parses file and formats it using the XMLProducer *
*/
public static void main(String[] args) {
try {
// redirect stdout to stderr
PrintStream out = System.out;
System.setOut(System.err);
// parse the file
JSONParser parser = new JSONParser();
BEASTInterface beastObject = parser.parseFile(new File(args[0]));
// restore stdout
System.setOut(out);
if (args.length > 1) {
FileWriter outfile = new FileWriter(args[1]);
outfile.write(new XMLProducer().toXML(beastObject));
outfile.close();
} else {
System.out.println(new XMLProducer().toXML(beastObject));
}
} catch (Exception e) {
e.printStackTrace();
}
}
} // class JSONParser