package beast.util;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
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.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.sun.org.apache.xerces.internal.dom.CoreDocumentImpl;
import beast.core.BEASTInterface;
import beast.core.Input;
import beast.core.InputForAnnotatedConstructor;
import beast.core.Param;
import beast.core.util.Log;
/**
*
* Provides basic functions for variable substitution and plates.
*
* @author Remco Bouckaert
* @author Alexei Drummond
*/
public class XMLParserUtils {
final static public List<String> beastObjectNames = AddOnManager.find(beast.core.BEASTInterface.class, AddOnManager.IMPLEMENTATION_DIR);
/**
* Expand plates in XML by duplicating the containing XML and replacing
* the plate variable with the appropriate value.
*/
public static void processPlates(Document doc, String plateElementName) {
// process plate elements
final NodeList nodes = doc.getElementsByTagName(plateElementName);
// instead of processing all plates, process them one by one,
// then check recursively for new plates that could have been
// created when they are nested
if (nodes.getLength() > 0) {
Node node = nodes.item(0);
final String var = node.getAttributes().getNamedItem("var").getNodeValue();
final String rangeString = node.getAttributes().getNamedItem("range").getNodeValue();
if (node.getAttributes().getNamedItem("fragment") != null) {
final String fragmentID = node.getAttributes().getNamedItem("fragment").getNodeValue();
Node fragment = getElementById(doc, fragmentID);
if (fragment == null) {
throw new RuntimeException("plate refers to fragment with id='" + fragmentID + "' that cannot be found");
}
fragment = fragment.cloneNode(true);
node.getParentNode().replaceChild(fragment, node);
node = fragment;
}
final String[] valuesString = rangeString.split(",");
// interpret values in the range of form x:y as all numbers between x and y inclusive
List<String> vals = new ArrayList<>();
for (final String valueString : valuesString) {
if (valueString.indexOf(":") > 0) {
try {
String[] range = valueString.split(":");
int min = Integer.parseInt(range[0]);
int max = Integer.parseInt(range[1]);
for (int i = min; i <= max; i++) {
vals.add(String.valueOf(i));
}
} catch (NumberFormatException e) {
Log.warning.println("plate range value '" + valueString + "'contains a ':' but does not seem to be a range, (like 1:5).");
Log.warning.println("interpreting it as if it were not a range");
vals.add(valueString);
}
} else {
vals.add(valueString);
}
}
for (final String val : vals) {
// copy children
final NodeList children = node.getChildNodes();
for (int childIndex = 0; childIndex < children.getLength(); childIndex++) {
final Node child = children.item(childIndex);
final Node newChild = child.cloneNode(true);
replaceVariable(newChild, var, val);
node.getParentNode().insertBefore(newChild, node);
}
}
node.getParentNode().removeChild(node);
processPlates(doc,plateElementName);
}
} // processPlates
static Node getElementById(Document doc, String id) {
if (doc.getElementById(id) == null) {
registerIDs(doc, doc.getDocumentElement());
}
return doc.getElementById(id);
}
static void registerIDs(Document doc, Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
if (node.getAttributes().getNamedItem("id") != null) {
final String id = node.getAttributes().getNamedItem("id").getNodeValue();
((CoreDocumentImpl) doc).putIdentifier(id, (Element) node);
}
}
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
registerIDs(doc, children.item(i));
}
}
/** export DOM document to a file -- handy for debugging **/
public static void saveDocAsXML(Document doc, String filename) {
try {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
Result output = new StreamResult(new File(filename));
Source input = new DOMSource(doc);
transformer.transform(input, output);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @param node the node to do variable replacement in
* @param var the variable name to replace
* @param valueString the value to replace the variable name with
*/
public static void replaceVariable(final Node node, final String var, final String valueString) {
switch (node.getNodeType()) {
case Node.ELEMENT_NODE: {
final Element element = (Element) node;
final NamedNodeMap atts = element.getAttributes();
for (int i = 0; i < atts.getLength(); i++) {
final Attr attr = (Attr) atts.item(i);
if (attr.getValue().contains("$(" + var + ")")) {
String att = attr.getValue();
att = att.replaceAll("\\$\\(" + var + "\\)", valueString);
attr.setNodeValue(att);
}
}
}
case Node.CDATA_SECTION_NODE: {
String content = node.getTextContent();
if (content.contains("$(" + var + ")")) {
content = content.replaceAll("\\$\\(" + var + "\\)", valueString);
node.setNodeValue(content);
}
}
}
// process children
final NodeList children = node.getChildNodes();
for (int childIndex = 0; childIndex < children.getLength(); childIndex++) {
final Node child = children.item(childIndex);
replaceVariable(child, var, valueString);
}
} // replace
/** return list of input types specified by Inputs or Param annotations
* @param clazz Class to generate the list for
* @param beastObject instantiation of the class, or null if not available
* @return
* @throws InstantiationException
* @throws IllegalAccessException
* @throws SecurityException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
*/
public static List<InputType> listInputs(Class<?> clazz, BEASTInterface beastObject) throws InstantiationException , IllegalAccessException, IllegalArgumentException, NoSuchMethodException, SecurityException {
List<InputType> inputTypes = new ArrayList<>();
// First, collect Input members
try {
if (beastObject == null) {
beastObject = (BEASTInterface) clazz.newInstance();
}
List<Input<?>> inputs = null;
inputs = beastObject.listInputs();
for (Input<?> input : inputs) {
if (!(input instanceof InputForAnnotatedConstructor)) {
try {
// force class types to be determined
if (input.getType() == null) {
input.determineClass(beastObject);
}
inputTypes.add(new InputType(input.getName(), input.getType(), true, input.defaultValue));
} catch (Exception e) {
// seems safe to ignore
e.printStackTrace();
}
}
}
} catch (InstantiationException e) {
// this can happen if there is no constructor without arguments,
// e.g. when there are annotated constructors only
}
// Second, collect types of annotated constructor
Constructor<?>[] allConstructors = clazz.getDeclaredConstructors();
for (Constructor<?> ctor : allConstructors) {
Annotation[][] annotations = ctor.getParameterAnnotations();
List<Param> paramAnnotations = new ArrayList<>();
for (Annotation [] a0 : annotations) {
for (Annotation a : a0) {
if (a instanceof Param) {
paramAnnotations.add((Param) a);
}
}
}
Class<?>[] types = ctor.getParameterTypes();
Type[] gtypes = ctor.getGenericParameterTypes();
if (types.length > 0 && paramAnnotations.size() > 0) {
int offset = 0;
if (types.length == paramAnnotations.size() + 1) {
offset = 1;
}
for (int i = 0; i < paramAnnotations.size(); i++) {
Param param = paramAnnotations.get(i);
Type type = types[i + offset];
Class<?> clazz2 = null;
try {
clazz2 = Class.forName(type.getTypeName());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (clazz2.isAssignableFrom(List.class)) {
Type[] genericTypes2 = ((ParameterizedType) gtypes[i + offset]).getActualTypeArguments();
Class<?> theClass = (Class<?>) genericTypes2[0];
InputType t = new InputType(param.name(), theClass, false, param.defaultValue());
inputTypes.add(t);
} else {
InputType t = new InputType(param.name(), types[i + offset], false, param.defaultValue());
inputTypes.add(t);
}
}
}
}
return inputTypes;
}
// /** get value of the input of a beast object with name specified in input **/
// static Object getValue(BEASTInterface beastObject, InputType input) {
// if (input.isInput()) {
// // input represents simple Input
// return beastObject.getInput(input.getName()).get();
// } else {
// // input represents Param annotation
// String methodName = "get" +
// input.getName().substring(0, 1).toUpperCase() +
// input.getName().substring(1);
// Method method;
// try {
// method = beastObject.getClass().getMethod(methodName);
// return method.invoke(beastObject);
// } catch (NoSuchMethodException | SecurityException |IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
// Log.err.println("Programmer error: when getting here an InputType was identified, but no Input or getter for Param annotation found");
// e.printStackTrace();
// return null;
// }
// }
// }
/**
* find closest matching class to named class *
*/
static String guessClass(final String classname) {
String name = classname;
if (classname.contains(".")) {
name = classname.substring(classname.lastIndexOf('.') + 1);
}
int bestDistance = Integer.MAX_VALUE;
String closestName = null;
for (final String beastObject : beastObjectNames) {
final String classname2 = beastObject.substring(beastObject.lastIndexOf('.') + 1);
final int distance = getLevenshteinDistance(name, classname2);
if (distance < bestDistance) {
bestDistance = distance;
closestName = beastObject;
}
}
return closestName;
}
/**
* Compute edit distance between two strings = Levenshtein distance *
*/
public static int getLevenshteinDistance(final String s, final String t) {
if (s == null || t == null) {
throw new IllegalArgumentException("Strings must not be null");
}
final int n = s.length(); // length of s
final int m = t.length(); // length of t
if (n == 0) {
return m;
} else if (m == 0) {
return n;
}
int p[] = new int[n + 1]; //'previous' cost array, horizontally
int d[] = new int[n + 1]; // cost array, horizontally
int _d[]; //placeholder to assist in swapping p and d
// indexes into strings s and t
int i; // iterates through s
int j; // iterates through t
char t_j; // jth character of t
int cost; // cost
for (i = 0; i <= n; i++) {
p[i] = i;
}
for (j = 1; j <= m; j++) {
t_j = t.charAt(j - 1);
d[0] = j;
for (i = 1; i <= n; i++) {
cost = s.charAt(i - 1) == t_j ? 0 : 1;
// minimum of cell to the left+1, to the top+1, diagonally left and up +cost
d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
}
// copy current distance counts to 'previous row' distance counts
_d = p;
p = d;
d = _d;
}
// our last action in the above loop was to switch d and p, so p now
// actually has the most recent cost counts
return p[n];
}
}