package dk.brics.xact.operations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Stack;
import org.jaxen.BaseXPath;
import org.jaxen.DefaultNavigator;
import org.jaxen.JaxenConstants;
import org.jaxen.JaxenException;
import org.jaxen.JaxenRuntimeException;
import org.jaxen.NamespaceContext;
import org.jaxen.UnsupportedAxisException;
import org.jaxen.XPath;
import org.jaxen.function.StringFunction;
import org.jaxen.saxpath.SAXPathException;
import dk.brics.xact.AttrNode;
import dk.brics.xact.Element;
import dk.brics.xact.Node;
import dk.brics.xact.TempNode;
import dk.brics.xact.XML;
import dk.brics.xact.XMLXPathException;
import dk.brics.xact.wrappers.AttrNodeWrapper;
import dk.brics.xact.wrappers.AttributeGapWrapper;
import dk.brics.xact.wrappers.AttributeWrapper;
import dk.brics.xact.wrappers.CommentWrapper;
import dk.brics.xact.wrappers.ConcreteTempNodeWrapper;
import dk.brics.xact.wrappers.ElementWrapper;
import dk.brics.xact.wrappers.NodeWrapper;
import dk.brics.xact.wrappers.ProcessingInstructionWrapper;
import dk.brics.xact.wrappers.TempNodeWrapper;
import dk.brics.xact.wrappers.TemplateGapWrapper;
import dk.brics.xact.wrappers.TextWrapper;
import dk.brics.xact.wrappers.WrapperNodeVisitor;
/**
* XPath evaluator for XML templates.
* <p>
* Traversal uses mutable node wrappers (see {@link NodeWrapper}) such that the
* DAG gets expanded to a tree by need. Visited nodes are copied, so all selected
* nodes are guaranteed to be fresh objects.
*/
public class XMLNavigator {
private XMLNavigator() {}
/**
* Abstract result of evaluation.
*/
public static class Result {
private final XML initial;
/**
* Constructs a new result.
*/
Result(XML initial) {
this.initial = initial;
}
/**
* Returns the copy of the real initial context.
*/
public XML getInitial() {
return initial;
}
}
/**
* Single-node result.
*/
public static class NodeResult extends Result {
private final Node node;
/**
* Constructs a new result.
*/
public NodeResult(XML root, Node node) {
super(root);
this.node = node;
}
/**
* Returns the selected node.
*/
public Node getNode() {
return node;
}
}
/**
* Node list result.
*/
public static class NodeListResult extends Result {
private final List<Node> nodes;
/**
* Constructs a new result.
*/
public NodeListResult(XML root, List<Node> nodes) {
super(root);
this.nodes = nodes;
}
/**
* Returns the selected nodes.
*/
public List<Node> getNodes() {
return nodes;
}
}
/**
* Element list result.
*/
public static class ElementListResult extends Result {
private final List<Element> elements;
/**
* Constructs a new result.
*/
public ElementListResult(XML root, List<Element> elements) {
super(root);
this.elements = elements;
}
/**
* Returns the selected elements.
*/
public List<Element> getElements() {
return elements;
}
}
private static TempNode getRealFirstChild(ElementWrapper e) {
ConcreteTempNodeWrapper<? extends TempNode> c = e.getFirstChild();
return c != null ? c.getReal() : null;
}
/**
* Returns the selected nodes.
*/
public static NodeListResult selectNodes(XML context, String xpath, boolean remove_successors) {
try {
ElementWrapper root = makeRoot(context);
List<?> selected = prepare(xpath, root).selectNodes(root.getFirstChild()); // apparently, jaxen doesn't use deep recursive calls
List<Node> sel = copyNodes(root, selected, remove_successors);
return new NodeListResult(getRealFirstChild(root), sel);
} catch (JaxenException e) {
throw new XMLXPathException(e);
} catch (JaxenRuntimeException e) {
throw new XMLXPathException(e.getCause());
}
}
/**
* Returns the selected sequence of element nodes (ignoring other nodes).
*/
public static ElementListResult selectElements(XML context, String xpath, boolean remove_successors) {
NodeListResult r = selectNodes(context, xpath, remove_successors);
List<Element> es = new ArrayList<Element>();
for (Node n : r.nodes)
if (n.isElement())
es.add(n.asElement());
return new ElementListResult(r.getInitial(), es);
}
/**
* Returns the string values of the selected nodes.
*/
public static List<String> selectStrings(XML context, String xpath) {
try {
ElementWrapper root = makeRoot(context);
List<?> selected = prepare(xpath, root).selectNodes(root.getFirstChild());
List<String> res = new ArrayList<String>();
MyNavigator nav = new MyNavigator(root);
for (Object s : selected)
res.add(StringFunction.evaluate(s, nav));
return Collections.unmodifiableList(res);
} catch (JaxenException e) {
throw new XMLXPathException(e);
} catch (JaxenRuntimeException e) {
throw new XMLXPathException(e.getCause());
}
}
/**
* Returns the first selected node.
*/
public static NodeResult selectSingleNode(XML context, String xpath, boolean remove_successors) {
try {
ElementWrapper root = makeRoot(context);
List<Object> s = new ArrayList<Object>();
Object n = prepare(xpath, root).selectSingleNode(root.getFirstChild());
if (n == null)
throw new XMLXPathException("no node selected");
s.add(n);
List<Node> sel = copyNodes(root, s, remove_successors);
if (sel.isEmpty()) // root is removed by copyNodes if selected
throw new XMLXPathException("no node selected");
return new NodeResult(getRealFirstChild(root), sel.get(0));
} catch (JaxenException e) {
throw new XMLXPathException(e);
} catch (JaxenRuntimeException e) {
throw new XMLXPathException(e.getCause());
}
}
/**
* Returns the element node by the given ID.
*/
public static Element selectElementByID(XML context, String id, boolean remove_successors) {
// TODO: make a more robust solution than generating an xpath expression
if (!isValidXPathString(id))
throw new IllegalArgumentException(id + " is not a valid ID string");
return (Element) selectSingleNode(context, "//node()[@id='" + id + "']" , remove_successors).getNode();
}
/**
* Returns false if the given string contains a quotation mark ('single' or "double").
*/
public static boolean isValidXPathString(String s) {
for (int i=0; i<s.length(); i++) {
switch (s.charAt(i)) {
case '\'':
case '"':
return false;
}
}
return true;
}
/**
* Returns the string value of the first selected node.
*/
public static String stringValueOf(XML context, String xpath) {
try {
ElementWrapper root = makeRoot(context);
return prepare(xpath, root).stringValueOf(root.getFirstChild());
} catch (JaxenException e) {
throw new XMLXPathException(e);
} catch (JaxenRuntimeException e) {
throw new XMLXPathException(e.getCause());
}
}
/**
* Returns the boolean value of the first selected node.
*/
public static boolean booleanValueOf(XML context, String xpath) {
try {
ElementWrapper root = makeRoot(context);
return prepare(xpath, root).booleanValueOf(root.getFirstChild());
} catch (JaxenException e) {
throw new XMLXPathException(e);
} catch (JaxenRuntimeException e) {
throw new XMLXPathException(e.getCause());
}
}
/**
* Returns the number value of the first selected node.
*/
public static Number numberValueOf(XML context, String xpath) {
try {
ElementWrapper root = makeRoot(context);
return prepare(xpath, root).numberValueOf(root.getFirstChild());
} catch (JaxenException e) {
throw new XMLXPathException(e);
} catch (JaxenRuntimeException e) {
throw new XMLXPathException(e.getCause());
}
}
private static ElementWrapper makeRoot(XML context) {
return new ElementWrapper((Element)new Element("root").appendContent(context), null); // used as document root node
}
private static XPath prepare(String xpath, NodeWrapper<? extends Node> root) {
try {
MyNavigator nav = new MyNavigator(root);
XPath xp = nav.parseXPath(xpath); // TODO: cache results of XPath parsing (per thread)?
xp.setNamespaceContext(new NamespaceContext() {
public String translateNamespacePrefixToUri(String prefix) {
String ns = XML.getThreadNamespaceMap().get(prefix);
if (ns == null)
ns = XML.getNamespaceMap().get(prefix);
return ns;
}}
);
return xp;
} catch (SAXPathException e) {
throw new XMLXPathException("XPath error", e);
}
}
@SuppressWarnings("serial")
private static class MyNavigator extends DefaultNavigator {
private final NodeWrapper<? extends Node> root;
public MyNavigator(NodeWrapper<? extends Node> root) {
this.root = root;
}
public XPath parseXPath(String xpath) throws SAXPathException {
return new BaseXPath(xpath, this);
}
public boolean isAttribute(Object obj) {
return obj instanceof AttrNodeWrapper<?>;
}
public boolean isComment(Object obj) {
return obj instanceof CommentWrapper;
}
public boolean isDocument(Object obj) {
return obj == root;
}
public boolean isElement(Object obj) {
return obj instanceof ElementWrapper && obj != root;
}
public boolean isNamespace(Object obj) {
return false; // unreachable (namespace axis not supported)
}
public boolean isProcessingInstruction(Object obj) {
return obj instanceof ProcessingInstructionWrapper;
}
public boolean isText(Object obj) {
return obj instanceof TextWrapper;
}
public String getAttributeName(Object attr) {
return ((AttributeWrapper)attr).getReal().getLocalName();
}
public String getAttributeNamespaceUri(Object attr) {
return ((AttributeWrapper)attr).getReal().getNamespace();
}
public String getAttributeQName(Object attr) {
return getAttributeName(attr);
}
public String getAttributeStringValue(Object attr) {
return ((AttributeWrapper)attr).getReal().getValue();
}
public String getCommentStringValue(Object comment) {
return ((CommentWrapper)comment).getReal().getValue();
}
public String getElementName(Object element) {
return ((ElementWrapper)element).getReal().getLocalName();
}
public String getElementNamespaceUri(Object element) {
return ((ElementWrapper)element).getReal().getNamespace();
}
public String getElementQName(Object element) {
return getElementName(element);
}
public String getElementStringValue(Object element) {
return XMLPrinter.getElementStringValue(((ElementWrapper)element).getReal());
}
public String getNamespacePrefix(Object ns) {
return ""; // unreachable (namespace axis not supported)
}
public String getNamespaceStringValue(Object ns) {
return ""; // unreachable (namespace axis not supported)
}
public String getTextStringValue(Object text) {
return ((TextWrapper)text).getReal().getString();
}
@Override
public String getProcessingInstructionData(Object pi) {
String s = ((ProcessingInstructionWrapper)pi).getReal().getData();
int i = 0;
while (i < s.length() && " \t\n\r".indexOf(s.charAt(i)) != -1) // strip initial whitespace
i++;
return s.substring(i);
}
@Override
public String getProcessingInstructionTarget(Object pi) {
return ((ProcessingInstructionWrapper)pi).getReal().getTarget();
}
@Override
public Object getDocumentNode(Object node) {
return root;
}
@Override
public Iterator<?> getAttributeAxisIterator(Object node) {
if (node instanceof ElementWrapper) {
final ElementWrapper e = (ElementWrapper)node;
return new Iterator<AttrNodeWrapper<? extends AttrNode>>() {
AttrNodeWrapper<? extends AttrNode> next = e.getFirstAttribute();
public boolean hasNext() {
return next != null;
}
public AttrNodeWrapper<? extends AttrNode> next() {
if (next == null)
throw new NoSuchElementException();
AttrNodeWrapper<? extends AttrNode> a = next;
next = next.getNextAttribute();
return a;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
} else
return JaxenConstants.EMPTY_ITERATOR;
}
@Override
public Iterator<?> getChildAxisIterator(Object node) {
if (node instanceof ElementWrapper) {
final ElementWrapper e = (ElementWrapper)node;
return new Iterator<TempNodeWrapper<? extends TempNode>>() {
TempNodeWrapper<? extends TempNode> next = e.getFirstChild();
public boolean hasNext() {
return next != null;
}
public TempNodeWrapper<? extends TempNode> next() {
if (next == null)
throw new NoSuchElementException();
TempNodeWrapper<? extends TempNode> x = next;
next = next.getNextSibling();
return x;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
} else
return JaxenConstants.EMPTY_ITERATOR;
}
@Override
public Iterator<?> getFollowingSiblingAxisIterator(Object node) {
if (node instanceof TempNodeWrapper<?>) {
final TempNodeWrapper<? extends TempNode> t = (TempNodeWrapper<?>)node;
return new Iterator<TempNodeWrapper<? extends TempNode>>() {
TempNodeWrapper<? extends TempNode> next = t.getNextSibling();
public boolean hasNext() {
return next != null;
}
public TempNodeWrapper<? extends TempNode> next() {
if (next == null)
throw new NoSuchElementException();
TempNodeWrapper<? extends TempNode> x = next;
next = next.getNextSibling();
return x;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
} else
return JaxenConstants.EMPTY_ITERATOR;
}
@Override
public Object getElementById(Object node, String elementId) {
return super.getElementById(node, elementId); // TODO: support getElementById (assume that ID attributes are named 'id')
// build map for the entire template, cache at the original root node
}
@Override
public Iterator<?> getAncestorAxisIterator(Object node) throws UnsupportedAxisException {
throw new UnsupportedAxisException("ancestor"); // TODO: support (certain) backward axes?
}
@Override
public Iterator<?> getAncestorOrSelfAxisIterator(Object node) throws UnsupportedAxisException {
throw new UnsupportedAxisException("ancestor-or-self");
}
@Override
public Iterator<?> getNamespaceAxisIterator(Object node) throws UnsupportedAxisException {
throw new UnsupportedAxisException("namespace");
}
@Override
public Iterator<?> getParentAxisIterator(Object node) {
final ElementWrapper parent = ((NodeWrapper<?>)node).getParent();
if (parent != null) {
return Collections.singletonList(parent).iterator();
} else
return JaxenConstants.EMPTY_ITERATOR;
}
@Override
public Object getParentNode(Object node) { // required by jaxen to sort results
return ((NodeWrapper<?>)node).getParent();
}
@Override
public Iterator<?> getPrecedingAxisIterator(Object node) throws UnsupportedAxisException {
throw new UnsupportedAxisException("preceding");
}
@Override
public Iterator<?> getPrecedingSiblingAxisIterator(Object node) throws UnsupportedAxisException {
throw new UnsupportedAxisException("preceding-siblings");
}
}
/**
* Copies the wrapped nodes reachable from the root.
* @param root root wrapper
* @param selected selected node wrappers
* @param remove_successors if true, the fresh selected nodes are detached from their successors
* @return fresh selected nodes (the fresh root is at root.real)
*/
private static List<Node> copyNodes(NodeWrapper<? extends TempNode> root, List<?> selected, boolean remove_successors) {
final Set<?> selectedset = new HashSet<Object>(selected);
final Stack<Entry> stack = new Stack<Entry>(); // use heap stack
final LinkedList<Node> fresh_selected = new LinkedList<Node>();
stack.push(new Entry(Entry.Kind.START_NODE, root));
while (!stack.isEmpty()) {
Entry e = stack.pop();
switch (e.kind) {
case START_NODE:
stack.push(new Entry(Entry.Kind.END_NODE, e.node));
e.node.visitBy(new WrapperNodeVisitor() {
@Override public void visit(ElementWrapper w) {
if (w.getFirstAttrWrapper() != null)
stack.push(new Entry(Entry.Kind.START_NODE, w.getFirstAttrWrapper()));
if (w.getFirstChildWrapper() != null)
stack.push(new Entry(Entry.Kind.START_NODE, w.getFirstChildWrapper()));
}
});
e.node.visitBy(new WrapperNodeVisitor() {
@Override public void visit(AttrNodeWrapper<? extends AttrNode> w) {
if (w.getNextAttrWrapper() != null)
stack.push(new Entry(Entry.Kind.START_NODE, w.getNextAttrWrapper()));
}
@Override public void visit(TempNodeWrapper<? extends TempNode> w) {
if (w.getNextSiblingWrapper() != null)
stack.push(new Entry(Entry.Kind.START_NODE, w.getNextSiblingWrapper()));
}
});
break;
case END_NODE:
e.node.visitBy(new WrapperNodeVisitor() {
@Override public void visit(AttributeGapWrapper w) {
w.setReal(w.getReal().copy(w.getRealNextAttr()));
}
@Override public void visit(AttributeWrapper w) {
w.setReal(w.getReal().copy(w.getRealNextAttr()));
}
@Override public void visit(ElementWrapper w) {
w.setReal(w.getReal().copy(w.getRealFirstAttr(), w.getRealFirstChild(), w.getRealNextSibling()));
}
@Override public void visit(TemplateGapWrapper w) {
w.setReal(w.getReal().copy(w.getRealNextSibling()));
}
@Override public void visit(TextWrapper w) {
w.setReal(w.getReal().copy(w.getRealNextSibling()));
}
@Override public void visit(CommentWrapper w) {
w.setReal(w.getReal().copy(w.getRealNextSibling()));
}
@Override public void visit(ProcessingInstructionWrapper w) {
w.setReal(w.getReal().copy(w.getRealNextSibling()));
}
});
if (selectedset.contains(e.node) && e.node != root) {
Node en = e.node.getReal();
if (remove_successors) {
if (en instanceof TempNode) {
en = ((TempNode) en).copy(null);
} else if (en instanceof AttrNode) {
en = ((AttrNode) en).copy(null);
}
}
fresh_selected.addFirst(en);
}
break;
}
}
return fresh_selected;
}
static private class Entry {
enum Kind {START_NODE, END_NODE};
final Kind kind;
final NodeWrapper<? extends Node> node;
Entry(Kind kind, NodeWrapper<? extends Node> node) {
this.kind = kind;
this.node = node;
}
}
}