package beast.util;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
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.ParserConfigurationException;
import org.xml.sax.SAXException;
import beast.app.BEASTVersion2;
import beast.core.BEASTInterface;
import beast.core.Input;
import beast.core.State;
import beast.core.parameter.BooleanParameter;
import beast.core.parameter.IntegerParameter;
import beast.core.parameter.Parameter;
import beast.core.parameter.RealParameter;
import beast.evolution.alignment.Alignment;
import beast.evolution.tree.TraitSet;
/*
* Why JSON:
*
* JSON vs XML: http://www.json.org/fatfree.html
* + JSON more readable
* + JSON less to type
*
* JSON vs YAML: http://en.wikipedia.org/wiki/YAML
*
* + JSON has editor support -- gedit, any Javascript editor
* + JSON validation -- http://jsonlint.com/ for validation
* + JSON = Javascript, useful format for GUIs
* - JSON bracket matching is not well supported in editors
*
* + YAML no issues with brackets
* - YAML little editor support
*/
public class JSONProducer {
/**
* list of objects already converted to JSON, so an idref suffices
*/
Set<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 JSON *
*/
int indentCount;
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";
public JSONProducer() {
super();
}
/**
* Main entry point for this class
* Given a plug-in, produces the JSON in BEAST 2 format
* representing the plug-in. This assumes beastObject is Runnable
*/
public String toJSON(BEASTInterface beastObject) {
return toJSON(beastObject, new ArrayList<>());
}
public String toJSON(BEASTInterface beastObject, Collection<BEASTInterface> others) {
try {
StringBuffer buf = new StringBuffer();
//buf.append("{\"version\": \"2.0\",\n\"namespace\": \"" + DEFAULT_NAMESPACE + "\",\n\n" +
// "\"" + JSONParser.ANALYSIS_ELEMENT + "\": [\n");
buf.append("{version: \"" + (new BEASTVersion2()).getMajorVersion() + "\",\nnamespace: \"" + DEFAULT_NAMESPACE + "\",\n\n" +
XMLParser.BEAST_ELEMENT + ": [\n");
//buf.append("\n\n");
isDone = new HashSet<>();
isInputsDone = new HashMap<>();
IDs = new HashSet<>();
indentCount = 1;
List<BEASTInterface> priorityBeastObjects = new ArrayList<>();
findPriorityBeastObjects(beastObject, priorityBeastObjects);
for (BEASTInterface beastObject2 : priorityBeastObjects) {
if (!isDone.contains(beastObject2)) {
//name = name.substring(name.lastIndexOf('.') + 1).toLowerCase();
beastObjectToJSON(beastObject2, BEASTInterface.class, buf, null, true);
buf.append(",");
}
}
buf.append("\n\n");
beastObjectToJSON(beastObject, BEASTInterface.class, buf, null, true);
String end = "\n]\n}";
buf.append(end);
String JSON = buf.toString();
String[] nameSpaces = DEFAULT_NAMESPACE.split(":");
for (String nameSpace : nameSpaces) {
//JSON = JSON.replaceAll("\"spec\": \"" + nameSpace + ".", "\"spec\": \"");
JSON = JSON.replaceAll("spec: \"" + nameSpace + ".", "spec: \"");
}
return JSON;
} catch (Exception e) {
e.printStackTrace();
return null;
}
} // toJSON
private void findPriorityBeastObjects(BEASTInterface beastObject, List<BEASTInterface> priorityBeastObjects) throws IllegalArgumentException, IllegalAccessException {
if (beastObject.getClass().equals(Alignment.class)) {
priorityBeastObjects.add(beastObject);
}
if (beastObject instanceof TraitSet) {
priorityBeastObjects.add(beastObject);
}
for (BEASTInterface beastObject2 : beastObject.listActiveBEASTObjects()) {
findPriorityBeastObjects(beastObject2, priorityBeastObjects);
}
}
public String stateNodeToJSON(BEASTInterface beastObject) {
try {
StringBuffer buf = new StringBuffer();
isDone = new HashSet<>();
IDs = new HashSet<>();
indentCount = 1;
beastObjectToJSON(beastObject, null, buf, null, false);
return buf.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Produce JSON fragment for a beast object with given name, putting results in buf.
* It tries to create JSON conforming to the JSON transformation rules (see JSONParser)
* that is moderately readable.
*/
void beastObjectToJSON(BEASTInterface beastObject, Class<?> defaultType, StringBuffer buf, String name, boolean isTopLevel) {
// determine element name, default is input, otherwise find one of the defaults
String indent = "";
for (int i = 0; i < indentCount; i++) {
indent += "\t";
//buf.append(" ");
}
indentCount++;
// open element
boolean needsComma = false;
if (name != null) {
//buf.append((indentCount == 1 ? "" : indent.substring(1)) + " \"" + name + "\": {");
buf.append((indentCount == 1 ? "" : indent.substring(1)) + " " + name + ": {");
} else {
buf.append(indent + "{");
}
boolean skipInputs = false;
if (isDone.contains(beastObject)) {
// JSON is already produced, we can idref it
buf.append((needsComma == true) ? ",\n" + indent + " " : "");
buf.append("idref: \"" + beastObject.getID() + "\" ");
needsComma = true;
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 (IDs.contains(id)) {
int k = 1;
while (IDs.contains(id + k)) {
k++;
}
id = id + k;
}
buf.append((needsComma == true) ? ",\n" + indent + " " : "");
buf.append("id: \"" + normalise(null, id) + "\"");
needsComma = true;
IDs.add(id);
}
isDone.add(beastObject);
}
String className = beastObject.getClass().getName();
if (skipInputs == false) {
// only add spec element if it cannot be deduced otherwise (i.e., by idref)
if (defaultType != null && !defaultType.getName().equals(className)) {
buf.append((needsComma == true) ? ",\n" + indent + " " : "");
//buf.append("\"spec\": \"" + className + "\"");
buf.append("spec: \"" + className + "\"");
needsComma = true;
}
}
if (!skipInputs) {
// process inputs of this beastObject
// first, collect values as attributes
List<Input<?>> inputs = beastObject.listInputs();
//List<InputType> inputs = XMLParserUtils.listInputs(beastObject.getClass(), beastObject);
for (Input<?> input : inputs) {
StringBuffer buf2 = new StringBuffer();
Object value = input.get();
inputToJSON(input, value, beastObject, buf2, true, indent);
if (buf2.length() > 0) {
buf.append((needsComma == true) ? "," : "");
buf.append(buf2);
needsComma = true;
}
}
// next, collect values as input elements
StringBuffer buf2 = new StringBuffer();
for (Input<?> input : inputs) {
StringBuffer buf3 = new StringBuffer();
Object value = input.get();
inputToJSON(input, value, beastObject, buf3, false, indent);
if (buf3.length() > 0) {
buf2.append((needsComma == true) ? ",\n" : "\n");
buf2.append(buf3);
needsComma = true;
}
}
if (buf2.length() != 0) {
buf.append(buf2);
}
indentCount--;
if (needsComma) {
buf.append("\n"+indent);
}
needsComma = true;
} else {
// close element
indentCount--;
buf.append("");
needsComma = true;
}
//if (m_nIndent < 2) {
// collapse newlines if there are no sub-objects
String str = buf.toString();
if (str.indexOf('}') < 0 && str.length() < 1024) {
str = str.replaceAll("\\s+", " ");
buf.delete(0, buf.length());
buf.append(indent);
buf.append(str);
}
buf.append("}");
//}
} // beastObjectToJSON
/**
* produce JSON for an input of a beastObject, both as attribute/value pairs for
* primitive inputs (if isShort=true) and as individual elements (if isShort=false)
*
* @param input0: name of the input
* @param beastObject: beastObject to produce this input JSON for
* @param buf: gets JSON results are appended
* @param isShort: flag to indicate attribute/value format (true) or element format (false)
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private void inputToJSON(Input input, Object value, BEASTInterface beastObject, StringBuffer buf, boolean isShort, String indent) {
if (value != null) {
// distinguish between Map, List, BEASTObject and primitive input types
if (value instanceof Map) {
if (!isShort) {
Map<String,?> map = (Map<String,?>) value;
StringBuffer buf2 = new StringBuffer();
// determine label width
int whiteSpaceWidth = 0;
for (String key : map.keySet()) {
whiteSpaceWidth = Math.max(whiteSpaceWidth, key.length());
}
boolean needsComma = false;
List<String> keys = new ArrayList<>();
keys.addAll(map.keySet());
Collections.sort(keys);
for (String key : keys) {
if (needsComma) {
buf2.append(",\n");
}
buf2.append(indent + " " + key);
for (int k = key.length(); k < whiteSpaceWidth; k++) {
buf2.append(' ');
}
buf2.append(" :\"" + map.get(key) +"\"");
needsComma = true;
}
buf.append(buf2);
}
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) {
StringBuffer buf2 = new StringBuffer();
//buf2.append(indent + " \"" + input0 + "\": [\n");
buf2.append(indent + " " + input.getName() + ": [\n");
boolean needsComma = false;
int oldLen = buf2.length();
for (Object o2 : (List) value) {
if (needsComma) {
buf2.append(",\n");
}
StringBuffer buf3 = new StringBuffer();
if (o2 instanceof BEASTInterface) {
beastObjectToJSON((BEASTInterface) o2, input.getType(), buf3, null, false);
} else {
buf2.append(o2.toString());
}
buf2.append(buf3);
needsComma = oldLen < buf2.length();
}
if (buf2.length() != oldLen) {
buf.append(buf2);
buf.append("\n" + indent + " ]");
}
}
return;
} else if (value instanceof BEASTInterface) {
if (!value.equals(input.defaultValue)) {
// Parameters can use short hand notation if they are not in the state
// Note this means lower and upper bounds are lost -- no problem for BEAST, but maybe for BEAUti
if (value instanceof BooleanParameter || value instanceof IntegerParameter || value instanceof RealParameter) {
Parameter.Base parameter = (Parameter.Base) value;
boolean isInState = false;
for (Object o : parameter.getOutputs()) {
if (o instanceof State) {
isInState = true;
break;
}
}
if (!isInState && parameter.getDimension() == 1 && parameter.getMinorDimension1() == 1) {
// if not in state, bounds do not matter
//if ((parameter instanceof RealParameter && parameter.getLower().equals(Double.NEGATIVE_INFINITY) && parameter.getUpper().equals(Double.POSITIVE_INFINITY)) ||
// (parameter instanceof IntegerParameter && parameter.getLower().equals(Integer.MIN_VALUE + 1) && parameter.getUpper().equals(Integer.MAX_VALUE - 1))) {
if (isShort) {
buf.append(" " + input.getName() + ": \"" + parameter.getValue() + "\"");
} else {
return;
}
//}
}
}
if (isShort && isDone.contains(value)) {
buf.append(" " + input.getName() + ": \"@" + ((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()))) {
beastObjectToJSON((BEASTInterface) value, input.getType(), buf, input.getName(), false);
}
}
return;
} else {
// primitive type
if (!value.equals(input.defaultValue)) {
String valueString = value.toString();
if (isShort) {
if (valueString.indexOf('\n') < 0) {
buf.append(" " + input.getName() + ": " + normalise(input, value.toString()) + "");
}
} else {
if (valueString.indexOf('\n') >= 0) {
buf.append(indent + "" + input.getName() + ": " + normalise(input, value.toString()) + "");
}
}
}
return;
}
} else {
// value=null, no JSON to produce
return;
}
} // inputToJSON
/** convert plain text string to JSON string, replacing some entities **/
private String normalise(Input<?> input, String str) {
str = str.replaceAll("\\\\", "\\\\\\\\");
str = str.replaceAll("/", "\\\\/");
str = str.replaceAll("\b", "\\\\b");
str = str.replaceAll("\f", "\\\\f");
str = str.replaceAll("\t", "\\\\t");
str = str.replaceAll("\\r", "\\\\r");
str = str.replaceAll("\"", "\\\\\"");
str = str.replaceAll("\n", "\\\\n");
if (input != null && !input.getType().equals(Double.class) &&
!input.getType().equals(Integer.class)&&
!input.getType().equals(Boolean.class)) {
str = "\"" + str + "\"";
}
return str;
}
public static void main(String[] args) throws SAXException, IOException, ParserConfigurationException, XMLParserException {
// convert BEAST 2 XML to BEAST JSON file
XMLParser parser = new XMLParser();
BEASTInterface beastObject = parser.parseFile(new File(args[0]));
String JSONFile = args[0].replace(".xml", ".json");
PrintStream out;
if (JSONFile.endsWith(".json")) {
out = new PrintStream(new File(JSONFile));
} else {
out = System.out;
}
JSONProducer writer = new JSONProducer();
String JSON = writer.toJSON(beastObject);
out.println(JSON);
out.close();
}
}