net.sf.saxon.tree.util.Navigator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Saxon-HE Show documentation
Show all versions of Saxon-HE Show documentation
The XSLT and XQuery Processor
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2023 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package net.sf.saxon.tree.util;
import net.sf.saxon.Controller;
import net.sf.saxon.event.Outputter;
import net.sf.saxon.event.Receiver;
import net.sf.saxon.event.ReceiverOption;
import net.sf.saxon.expr.Expression;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.functions.Reverse;
import net.sf.saxon.om.*;
import net.sf.saxon.pattern.*;
import net.sf.saxon.s9api.Location;
import net.sf.saxon.str.UnicodeString;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.tree.iter.AxisIterator;
import net.sf.saxon.tree.iter.EmptyIterator;
import net.sf.saxon.tree.iter.SingleNodeIterator;
import net.sf.saxon.tree.jiter.WrappingJavaIterator;
import net.sf.saxon.tree.tiny.TinyElementImpl;
import net.sf.saxon.tree.tiny.TinyNodeImpl;
import net.sf.saxon.tree.tiny.TinyTextualElement;
import net.sf.saxon.tree.wrapper.SiblingCountingNode;
import net.sf.saxon.tree.wrapper.VirtualCopy;
import net.sf.saxon.type.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Predicate;
/**
* The Navigator class provides helper classes for navigating a tree, irrespective
* of its implementation
*
*/
public final class Navigator {
// Class is never instantiated
private Navigator() {
}
/**
* Get the string value of an attribute of a given element, given the URI and
* local part of the attribute name.
*
* @param element the element on which the required attribute appears
* @param uri The namespace URI of the attribute name.
* The "no namespace" case is represented as an empty string.
* @param localName The local part of the attribute name.
* @return the attribute value, or null if the attribute is not present
* @since 9.0
*/
public static String getAttributeValue(NodeInfo element, NamespaceUri uri, String localName) {
return element.getAttributeValue(uri, localName);
}
/**
* Get the string value of an inherited attribute of a given element, given the URI and
* local part of the attribute name. If the element does not have such an attribute, go up
* the ancestor axis to find such an attribute
*
* @param element the element where the search should start
* @param uri The namespace URI of the attribute name.
* The "no namespace" case is represented as an empty string.
* @param localName The local part of the attribute name.
* @return the attribute value, or null if the attribute is not present on any ancestor
* @since 9.9
*/
public static String getInheritedAttributeValue(NodeInfo element, NamespaceUri uri, String localName) {
NodeInfo node = element;
while (node != null) {
String value = node.getAttributeValue(uri, localName);
if (value == null) {
node = node.getParent();
} else {
return value;
}
}
return null;
}
/**
* Helper method to get the name of a node as a structuredQName. Used from bytecode
* @param node the node
* @return the name of the node if it has a name, or null otherwise
*/
public static StructuredQName getNodeName(NodeInfo node) {
if (node.getLocalPart() != null) {
return new StructuredQName(node.getPrefix(), node.getNamespaceUri(), node.getLocalPart());
} else {
return null;
}
}
/**
* Helper method to get the outermost element of a document, given the document node
*
* @param doc the document node at the root of the document
* @return the first element child of the document node, if there is one, else null. This
* is often referred to as the "root element" or "document element". No error is reported
* if the document node has multiple element children, which can happen in a document
* constructed using XSLT or XQuery.
* @since 9.3
*/
/*@Nullable*/
public static NodeInfo getOutermostElement(/*@NotNull*/ TreeInfo doc) {
return doc.getRootNode().iterateAxis(AxisInfo.CHILD, NodeKindTest.ELEMENT).next();
}
/**
* Helper method to get the base URI of an element or processing instruction node
*
* @param node the node whose base URI is required
* @return the base URI of the node
* @since 8.7
*/
/*@Nullable*/
public static String getBaseURI(/*@NotNull*/ NodeInfo node) {
return getBaseURI(node, n -> {
NodeInfo parent = n.getParent();
return parent == null || !parent.getSystemId().equals(n.getSystemId());
});
}
/**
* Helper method to get the base URI of an element or processing instruction node
* @param node the node whose base URI is required
* @param isTopElementWithinEntity a predicate applied to a node that can be tested to determine
* if the node is an element that originated as the outermost element within
* an external entity. If this is the case, the rules for determining
* the base URI are slightly different; in particular, any @xml:base
* attribute on such a node is interpreted relative to the system ID of
* the entity, not relative to the base URI of the parent node.
* @return the base URI of the node
* @since 9.9
*/
/*@Nullable*/
public static String getBaseURI(/*@NotNull*/ NodeInfo node, Predicate isTopElementWithinEntity) {
String xmlBase = node instanceof TinyElementImpl ?
((TinyElementImpl)node).getAttributeValue(StandardNames.XML_BASE) :
node.getAttributeValue(NamespaceUri.XML, "base");
if (xmlBase != null) {
URI baseURI;
try {
baseURI = new URI(xmlBase);
if (baseURI.isAbsolute()) {
return xmlBase;
} else {
NodeInfo parentNode = node.getParent();
if (parentNode == null) {
// We have a parentless element with a relative xml:base attribute.
// See for example test XQTS fn-base-uri-10 and base-uri-27
URI base2 = new URI(node.getSystemId());
URI resolved = xmlBase.isEmpty() ? base2 : base2.resolve(baseURI);
return resolved.toString();
}
String startSysId = node.getSystemId();
if (startSysId == null) {
return null;
}
String parentSysId = parentNode.getSystemId();
boolean isTopWithinEntity = false; // TODO: variable is unused. What's going on here? - MHK 2020-07-04
if (node instanceof TinyElementImpl) {
isTopWithinEntity = ((TinyElementImpl)node).getTree().isTopWithinEntity(((TinyElementImpl) node).getNodeNumber());
} else {
isTopWithinEntity = !startSysId.equals(parentSysId);
}
URI base = new URI(isTopElementWithinEntity.test(node) ? startSysId : parentNode.getBaseURI());
//URI base = new URI(parent.getBaseURI()); //bug 3530
baseURI = xmlBase.isEmpty() ? base : base.resolve(baseURI);
}
} catch (URISyntaxException e) {
// xml:base is an invalid URI. Just return it as is: the operation that needs the base URI
// will probably fail as a result. \
return xmlBase;
}
return baseURI.toString();
}
String startSystemId = node.getSystemId();
if (startSystemId == null) {
return null;
}
NodeInfo parent = node.getParent();
if (parent == null) {
return startSystemId;
}
String parentSystemId = parent.getSystemId();
if (startSystemId.equals(parentSystemId) || parentSystemId.isEmpty()) {
return parent.getBaseURI();
} else {
return startSystemId;
}
}
/**
* Get an absolute XPath expression that identifies a given node within its document
*
* @param node the node whose path is required. If null is supplied,
* an empty string is returned - this fact is used in making a recursive call
* for a parentless node.
* @return a path expression that can be used to retrieve the node
*/
public static String getPath(NodeInfo node) {
return getPath(node, null);
}
/**
* Get an absolute XPath expression that identifies a given node within its document. The
* resulting path is intended for human readers, not for software evaluation. It uses lexical
* QNames (prefix:localname) for element and attribute names, with the original prefix as it
* appears in the source document. The sibling position of an element (a/b[2]) is included
* unless the node is a streamed node.
*
* @param node the node whose path is required. If null is supplied,
* an empty string is returned - this fact is used in making a recursive call
* for a parentless node.
* @param context the XPath dynamic evaluation context. May be null if no context is known
* @return a path expression that can be used to retrieve the node
*/
public static String getPath(/*@Nullable*/ NodeInfo node, /*@Nullable*/ XPathContext context) {
if (node == null) {
return "";
}
String pre;
boolean streamed = node.getConfiguration().isStreamedNode(node);
NodeInfo parent = node.getParent();
// System.err.println("node = " + node + " parent = " + parent);
switch (node.getNodeKind()) {
case Type.DOCUMENT:
return "/";
case Type.ELEMENT:
if (parent == null) {
return node.getDisplayName();
} else {
pre = getPath(parent, context);
if (pre.equals("/")) {
return '/' + node.getDisplayName();
} else {
return pre + '/' + node.getDisplayName() + (streamed ? "" : "[" + getNumberSimple(node, context) + "]");
}
}
case Type.ATTRIBUTE:
return getPath(parent, context) + "/@" + node.getDisplayName();
case Type.TEXT:
pre = getPath(parent, context);
return (pre.equals("/") ? "" : pre) +
"/text()" + (streamed ? "" : "[" + getNumberSimple(node, context) + "]");
case Type.COMMENT:
pre = getPath(parent, context);
return (pre.equals("/") ? "" : pre) +
"/comment()" + (streamed ? "" : "[" + getNumberSimple(node, context) + "]");
case Type.PROCESSING_INSTRUCTION:
pre = getPath(parent, context);
return (pre.equals("/") ? "" : pre) +
"/processing-instruction()" + (streamed ? "" : "[" + getNumberSimple(node, context) + "]");
case Type.NAMESPACE:
String test = node.getLocalPart();
if (test.isEmpty()) {
// default namespace: need a node-test that selects unnamed nodes only
test = "*[not(local-name()]";
}
return getPath(parent, context) + "/namespace::" + test;
default:
return "";
}
}
/**
* Get the absolute path to a node
*
* @param node the node in question
* @return an object representing the path to the node
*/
public static AbsolutePath getAbsolutePath(NodeInfo node) {
boolean streamed = node.getConfiguration().isStreamedNode(node);
LinkedList path = new LinkedList<>();
String sysId = node.getSystemId();
while (node != null && node.getNodeKind() != Type.DOCUMENT) {
path.addFirst(new AbsolutePath.PathElement(node.getNodeKind(),
NameOfNode.makeName(node),
streamed ? -1 : getNumberSimple(node, null)));
node = node.getParent();
}
AbsolutePath a = new AbsolutePath(path);
a.setSystemId(sysId);
return a;
}
/**
* Ask whether two nodes have the same name
* @param n1 the first node
* @param n2 the second node
* @return true if they have the same namespace and local part
*/
public static boolean haveSameName(NodeInfo n1, NodeInfo n2) {
if (n1.hasFingerprint() && n2.hasFingerprint()) {
return n1.getFingerprint() == n2.getFingerprint();
} else {
return n1.getLocalPart().equals(n2.getLocalPart()) &&
n1.getNamespaceUri().equals(n2.getNamespaceUri());
}
}
/**
* Get simple node number. This is defined as one plus the number of previous siblings of the
* same node type and name. It is not accessible directly in XSL.
*
* @param node The node whose number is required
* @param context Used for remembering previous result, for
* performance. May be null.
* @return the node number, as defined above
*/
public static int getNumberSimple(/*@NotNull*/ NodeInfo node, /*@Nullable*/ XPathContext context) {
//checkNumberable(node);
NodeTest same;
if (node.getLocalPart().isEmpty()) {
same = NodeKindTest.makeNodeKindTest(node.getNodeKind());
} else {
same = new SameNameTest(node);
}
Controller controller = context == null ? null : context.getController();
AxisIterator preceding = node.iterateAxis(AxisInfo.PRECEDING_SIBLING, same);
int i = 1;
while (true) {
NodeInfo prev = preceding.next();
if (prev == null) {
break;
}
if (controller != null) {
int memo = controller.getRememberedNumber(prev);
if (memo > 0) {
memo += i;
controller.setRememberedNumber(node, memo);
return memo;
}
}
i++;
}
if (controller != null) {
controller.setRememberedNumber(node, i);
}
return i;
}
/**
* Get node number (level="single"). If the current node matches the supplied pattern, the returned
* number is one plus the number of previous siblings that match the pattern. Otherwise,
* return the element number of the nearest ancestor that matches the supplied pattern.
*
* @param node the current node, the one whose node number is required
* @param count Pattern that identifies which nodes should be
* counted. Default (null) is the element name if the current node is
* an element, or "node()" otherwise.
* @param from Pattern that specifies where counting starts from.
* Default (null) is the root node. (This parameter does not seem
* useful but is included for the sake of XSLT conformance.)
* @param context the dynamic context of the transformation, used if
* the patterns reference context values (e.g. variables)
* @return the node number established as follows: go to the nearest
* ancestor-or-self that matches the 'count' pattern and that is a
* descendant of the nearest ancestor that matches the 'from' pattern.
* Return one plus the nunber of preceding siblings of that ancestor
* that match the 'count' pattern. If there is no such ancestor,
* return 0.
* @throws XPathException when any error occurs in processing
*/
public static int getNumberSingle(/*@NotNull*/ NodeInfo node, /*@Nullable*/ Pattern count,
/*@Nullable*/ Pattern from, XPathContext context) throws XPathException {
if (count == null && from == null) {
return getNumberSimple(node, context);
}
boolean knownToMatch = false;
if (count == null) {
if (node.getLocalPart().isEmpty()) { // unnamed node
count = new NodeTestPattern(NodeKindTest.makeNodeKindTest(node.getNodeKind()));
} else {
count = new NodeTestPattern(new SameNameTest(node));
}
knownToMatch = true;
}
NodeInfo target = node;
// code changed in 9.5 to fix issue described in spec bug 9840
if (!knownToMatch) {
while (true) {
if (count.matches(target, context)) {
if (from == null) {
break;
} else {
// see whether there is an ancestor node that matches the from pattern
NodeInfo anc = target;
while (!from.matches(anc, context)) {
anc = anc.getParent();
if (anc == null) {
// there's no ancestor that matches the "from" pattern
return 0;
}
}
// we've found the node to be counted
break;
}
} else if (from != null && from.matches(target, context)) {
// if we find something that matches "from" before we find something that matches "count", exit
return 0;
} else {
target = target.getParent();
if (target == null) {
// found the root before finding a match on either "count" or "from"
return 0;
}
}
}
}
// we've found the ancestor to count from
SequenceIterator preceding =
target.iterateAxis(AxisInfo.PRECEDING_SIBLING, getNodeTestForPattern(count));
// pass the filter condition down to the axis enumeration where possible
boolean alreadyChecked = count instanceof NodeTestPattern;
int i = 1;
while (true) {
NodeInfo p = (NodeInfo) preceding.next();
if (p == null) {
return i;
}
if (alreadyChecked || count.matches(p, context)) {
i++;
}
}
}
/**
* Get node number (level="any").
* Return one plus the number of previous nodes in the
* document that match the supplied pattern
*
* @param inst Identifies the xsl:number expression; this is relevant
* when the function is memoised to support repeated use of the same
* instruction to number multiple nodes
* @param node The node being numbered
* @param count Pattern that identifies which nodes should be
* counted. Default (null) is the element name if the current node is
* an element, or "node()" otherwise.
* @param from Pattern that specifies where counting starts from.
* Default (null) is the root node. Only nodes at or after the first (most
* recent) node that matches the 'from' pattern are counted.
* @param context The dynamic context for the transformation
* @param hasVariablesInPatterns if the count or from patterns
* contain variables, then it's not safe to get the answer by adding
* one to the number of the most recent node that matches
* @return one plus the number of nodes that precede the current node,
* that match the count pattern, and that follow the first node that
* matches the from pattern if specified.
* @throws net.sf.saxon.trans.XPathException
* if any dynamic error occurs
*/
public static int getNumberAny(/*@NotNull*/ Expression inst, /*@NotNull*/ NodeInfo node, /*@Nullable*/ Pattern count,
/*@Nullable*/ Pattern from, /*@NotNull*/ XPathContext context, boolean hasVariablesInPatterns) throws XPathException {
NodeInfo memoNode = null;
int memoNumber = 0;
Controller controller = context.getController();
assert controller != null;
boolean memoise = !hasVariablesInPatterns && from == null;
if (memoise) {
Object[] memo = (Object[]) controller.getUserData(inst.getLocation(), "xsl:number");
if (memo != null) {
memoNode = (NodeInfo) memo[0];
memoNumber = (Integer) memo[1];
}
}
int num = 0;
if (count == null) {
if (node.getLocalPart().isEmpty()) { // unnamed node
count = new NodeTestPattern(NodeKindTest.makeNodeKindTest(node.getNodeKind()));
} else {
count = new NodeTestPattern(new SameNameTest(node));
}
num = 1;
} else if (count.matches(node, context)) {
num = 1;
}
// We use a special axis invented for the purpose: the union of the preceding and
// ancestor axes, but in reverse document order
// Pass part of the filtering down to the axis iterator if possible
NodeTest filter;
if (from == null) {
filter = getNodeTestForPattern(count);
} else if (from.getUType() == UType.ELEMENT && count.getUType() == UType.ELEMENT) {
filter = NodeKindTest.ELEMENT;
} else {
filter = AnyNodeTest.getInstance();
}
if (from != null && from.matches(node, context)) {
return num;
}
SequenceIterator preceding =
node.iterateAxis(AxisInfo.PRECEDING_OR_ANCESTOR, filter);
while (true) {
NodeInfo prev = (NodeInfo) preceding.next();
if (prev == null) {
break;
}
if (count.matches(prev, context)) {
if (num == 1 && prev.equals(memoNode)) {
num = memoNumber + 1;
break;
}
num++;
}
if (from != null && from.matches(prev, context)) {
break;
}
}
if (memoise) {
Object[] memo = new Object[2];
memo[0] = node;
memo[1] = num;
controller.setUserData(inst.getLocation(), "xsl:number", memo);
}
return num;
}
/**
* Get node number (level="multiple").
* Return a vector giving the hierarchic position of this node. See the XSLT spec for details.
*
* @param node The node to be numbered
* @param count Pattern that identifies which nodes (ancestors and
* their previous siblings) should be counted. Default (null) is the
* element name if the current node is an element, or "node()"
* otherwise.
* @param from Pattern that specifies where counting starts from.
* Default (null) is the root node. Only nodes below the first (most
* recent) node that matches the 'from' pattern are counted.
* @param context The dynamic context for the transformation
* @return a vector containing for each ancestor-or-self that matches the
* count pattern and that is below the nearest node that matches the
* from pattern, an Integer which is one greater than the number of
* previous siblings that match the count pattern.
* @throws XPathException if an error occurs
*/
/*@NotNull*/
public static List getNumberMulti(/*@NotNull*/ NodeInfo node, /*@Nullable*/ Pattern count,
/*@Nullable*/ Pattern from, XPathContext context) throws XPathException {
//checkNumberable(node);
ArrayList v = new ArrayList<>(5);
if (count == null) {
if (node.getLocalPart().isEmpty()) { // unnamed node
count = new NodeTestPattern(NodeKindTest.makeNodeKindTest(node.getNodeKind()));
} else {
count = new NodeTestPattern(new SameNameTest(node));
}
}
NodeInfo curr = node;
while (true) {
if (count.matches(curr, context)) {
int num = getNumberSingle(curr, count, null, context);
v.add(0, (long) num);
}
if (from != null && from.matches(curr, context)) {
break;
}
curr = curr.getParent();
if (curr == null) {
break;
}
}
return v;
}
/**
* Get a NodeTest to use as a filter for nodes, given a pattern.
*/
private static NodeTest getNodeTestForPattern(Pattern pattern) {
ItemType type = pattern.getItemType();
if (type instanceof NodeTest) {
return (NodeTest) type;
} else if (pattern.getUType().overlaps(UType.ANY_NODE)) {
return AnyNodeTest.getInstance();
} else {
return ErrorType.getInstance();
}
}
/**
* Generic (model-independent) implementation of deep copy algorithm for nodes.
* This is available for use by any node implementations that choose to use it.
*
* @param node The node to be copied.
* @param out The receiver to which events will be sent
* @param copyOptions Options for copying namespaces, type annotations, etc,
* as defined in {@link net.sf.saxon.om.CopyOptions}
* @param locationId The location of the instruction invoking the copy
* @throws XPathException on any failure reported by the Receiver
* @throws IllegalArgumentException if the node is an attribute or namespace node
*/
public static void copy(/*@NotNull*/ NodeInfo node,
/*@NotNull*/ Receiver out,
int copyOptions,
Location locationId) throws XPathException {
switch (node.getNodeKind()) {
case Type.DOCUMENT: {
out.startDocument(CopyOptions.getStartDocumentProperties(copyOptions));
for (NodeInfo child : node.children()) {
child.copy(out, copyOptions, locationId);
}
out.endDocument();
break;
}
case Type.ELEMENT: {
SchemaType annotation = (copyOptions & CopyOptions.TYPE_ANNOTATIONS) != 0 ?
node.getSchemaType() :
Untyped.getInstance();
NamespaceMap ns = CopyOptions.includes(copyOptions, CopyOptions.ALL_NAMESPACES) ? node.getAllNamespaces() : NamespaceMap.emptyMap();
out.startElement(NameOfNode.makeName(node), annotation,
node.attributes(), ns,
locationId, ReceiverOption.BEQUEATH_INHERITED_NAMESPACES_ONLY | ReceiverOption.NAMESPACE_OK);
// output the children
for (NodeInfo child : node.children()) {
child.copy(out, copyOptions, locationId);
}
// finally the end tag
out.endElement();
return;
}
case Type.ATTRIBUTE: {
throw new IllegalArgumentException("Cannot copy attribute to Receiver");
// SimpleType annotation = (copyOptions & CopyOptions.TYPE_ANNOTATIONS) != 0 ?
// (SimpleType) node.getSchemaType() :
// BuiltInAtomicType.UNTYPED_ATOMIC;
// ((ComplexContentOutputter)out).attribute(NameOfNode.makeName(node), annotation, node.getStringValueCS(), locationId, ReceiverOption.NONE);
// return;
}
case Type.TEXT: {
UnicodeString value = node.getUnicodeStringValue();
if (value.length() != 0) {
// zero-length text nodes can arise from external model wrappers
out.characters(value, locationId, ReceiverOption.NONE);
}
return;
}
case Type.COMMENT: {
out.comment(node.getUnicodeStringValue(), locationId, ReceiverOption.NONE);
return;
}
case Type.PROCESSING_INSTRUCTION: {
out.processingInstruction(node.getLocalPart(), node.getUnicodeStringValue(), locationId, ReceiverOption.NONE);
return;
}
case Type.NAMESPACE: {
throw new IllegalArgumentException("Cannot copy namespace to Receiver");
// out.namespacesOLD(NamespaceMap.of(node.getLocalPart(), node.getStringValue()), ReceiverOption.NONE);
// return;
}
default:
}
}
/**
* Generic (model-independent) implementation of deep copy algorithm for nodes.
* This is available for use by any node implementations that choose to use it.
*
* @param node The node to be copied.
* @param out The outputter to which events will be sent
* @param copyOptions Options for copying namespaces, type annotations, etc,
* as defined in {@link net.sf.saxon.om.CopyOptions}
* @param locationId The location of the instruction invoking the copy
* @throws XPathException on any failure reported by the Receiver
*/
public static void copy(NodeInfo node, Outputter out,
int copyOptions, Location locationId) throws XPathException {
boolean keepTypes = (copyOptions & CopyOptions.TYPE_ANNOTATIONS) != 0;
switch (node.getNodeKind()) {
case Type.DOCUMENT: {
out.startDocument(CopyOptions.getStartDocumentProperties(copyOptions));
for (NodeInfo child : node.children()) {
copy(child, out, copyOptions, locationId);
}
out.endDocument();
break;
}
case Type.ELEMENT: {
SchemaType annotation = keepTypes ? node.getSchemaType() : Untyped.getInstance();
out.startElement(NameOfNode.makeName(node), annotation,
locationId, ReceiverOption.DISINHERIT_NAMESPACES | ReceiverOption.NAMESPACE_OK);
if ((copyOptions & CopyOptions.ALL_NAMESPACES) != 0) {
for (NamespaceBinding ns : node.getAllNamespaces()) {
out.namespace(ns.getPrefix(), ns.getNamespaceUri(), ReceiverOption.NONE);
}
}
for (AttributeInfo attr : node.attributes()) {
SimpleType attType = keepTypes ? attr.getType() : BuiltInAtomicType.UNTYPED_ATOMIC;
out.attribute(attr.getNodeName(), attType, attr.getValue(), attr.getLocation(), attr.getProperties());
}
// output the children
for (NodeInfo child : node.children()) {
copy(child, out, copyOptions, locationId);
}
// finally the end tag
out.endElement();
return;
}
case Type.ATTRIBUTE: {
SimpleType attType = keepTypes ? (SimpleType)node.getSchemaType() : BuiltInAtomicType.UNTYPED_ATOMIC;
out.attribute(NameOfNode.makeName(node), attType, node.getStringValue(), locationId, ReceiverOption.NONE);
return;
}
case Type.TEXT: {
UnicodeString value = node.getUnicodeStringValue();
if (value.length() != 0) {
// zero-length text nodes can arise from external model wrappers
out.characters(value, locationId, ReceiverOption.NONE);
}
return;
}
case Type.COMMENT: {
out.comment(node.getUnicodeStringValue(), locationId, ReceiverOption.NONE);
return;
}
case Type.PROCESSING_INSTRUCTION: {
out.processingInstruction(node.getLocalPart(), node.getUnicodeStringValue(), locationId, ReceiverOption.NONE);
return;
}
case Type.NAMESPACE: {
out.namespace(node.getLocalPart(), NamespaceUri.of(node.getStringValue()), ReceiverOption.NONE);
return;
}
default:
}
}
/**
* Generic (model-independent) method to determine the relative position of two
* nodes in document order. The nodes must be in the same tree.
*
* @param first The first node
* @param second The second node, whose position is to be compared with the first node
* @return -1 if this node precedes the other node, +1 if it follows the other
* node, or 0 if they are the same node. (In this case, isSameNode() will always
* return true, and the two nodes will produce the same result for generateId())
*/
public static int compareOrder(/*@NotNull*/ SiblingCountingNode first, /*@NotNull*/ SiblingCountingNode second) {
// are they the same node?
if (first.equals(second)) {
return 0;
}
NodeInfo firstParent = first.getParent();
if (firstParent == null) {
// first node is the root
return -1;
}
NodeInfo secondParent = second.getParent();
if (secondParent == null) {
// second node is the root
return +1;
}
// do they have the same parent (common case)?
if (firstParent.equals(secondParent)) {
int cat1 = nodeCategories[first.getNodeKind()];
int cat2 = nodeCategories[second.getNodeKind()];
if (cat1 == cat2) {
return first.getSiblingPosition() - second.getSiblingPosition();
} else {
return cat1 - cat2;
}
}
// find the depths of both nodes in the tree
int depth1 = 0;
int depth2 = 0;
NodeInfo p1 = first;
NodeInfo p2 = second;
while (p1 != null) {
depth1++;
p1 = p1.getParent();
}
while (p2 != null) {
depth2++;
p2 = p2.getParent();
}
// move up one branch of the tree so we have two nodes on the same level
p1 = first;
while (depth1 > depth2) {
p1 = p1.getParent();
assert p1 != null;
if (p1.equals(second)) {
return +1;
}
depth1--;
}
p2 = second;
while (depth2 > depth1) {
p2 = p2.getParent();
assert p2 != null;
if (p2.equals(first)) {
return -1;
}
depth2--;
}
// now move up both branches in sync until we find a common parent
while (true) {
NodeInfo par1 = p1.getParent();
NodeInfo par2 = p2.getParent();
if (par1 == null || par2 == null) {
throw new NullPointerException("Node order comparison - internal error");
}
if (par1.equals(par2)) {
if (p1.getNodeKind() == Type.ATTRIBUTE && p2.getNodeKind() != Type.ATTRIBUTE) {
return -1; // attributes first
}
if (p1.getNodeKind() != Type.ATTRIBUTE && p2.getNodeKind() == Type.ATTRIBUTE) {
return +1; // attributes first
}
return ((SiblingCountingNode) p1).getSiblingPosition() -
((SiblingCountingNode) p2).getSiblingPosition();
}
p1 = par1;
p2 = par2;
}
}
/**
* Generic (model-independent) method to determine the relative position of two
* node in document order. The nodes must be in the same tree.
*
* @param first The first node
* @param second The second node, whose position is to be compared with the first node
* @return {@link net.sf.saxon.om.AxisInfo#PRECEDING} if this node is on the preceding axis of the other node;
* {@link net.sf.saxon.om.AxisInfo#FOLLOWING} if it is on the following axis; {@link net.sf.saxon.om.AxisInfo#ANCESTOR} if the first node is an
* ancestor of the second; {@link net.sf.saxon.om.AxisInfo#DESCENDANT} if the first is a descendant of the second;
* {@link net.sf.saxon.om.AxisInfo#SELF} if they are the same node.
* @throws UnsupportedOperationException if either node is an attribute or namespace
* @since 9.5
*/
public static int comparePosition(/*@NotNull*/ NodeInfo first, /*@NotNull*/ NodeInfo second) {
if (first.getNodeKind() == Type.ATTRIBUTE || first.getNodeKind() == Type.NAMESPACE ||
second.getNodeKind() == Type.ATTRIBUTE || second.getNodeKind() == Type.NAMESPACE) {
throw new UnsupportedOperationException();
}
// are they the same node?
if (first.equals(second)) {
return AxisInfo.SELF;
}
NodeInfo firstParent = first.getParent();
if (firstParent == null) {
// first node is the root
return AxisInfo.ANCESTOR;
}
NodeInfo secondParent = second.getParent();
if (secondParent == null) {
// second node is the root
return AxisInfo.DESCENDANT;
}
// do they have the same parent (common case)?
if (firstParent.equals(secondParent)) {
if (first.compareOrder(second) < 0) {
return AxisInfo.PRECEDING;
} else {
return AxisInfo.FOLLOWING;
}
}
// find the depths of both nodes in the tree
int depth1 = 0;
int depth2 = 0;
NodeInfo p1 = first;
NodeInfo p2 = second;
while (p1 != null) {
depth1++;
p1 = p1.getParent();
}
while (p2 != null) {
depth2++;
p2 = p2.getParent();
}
// Test if either node is an ancestor of the other
p1 = first;
while (depth1 > depth2) {
p1 = p1.getParent();
assert p1 != null;
if (p1.equals(second)) {
return AxisInfo.DESCENDANT;
}
depth1--;
}
p2 = second;
while (depth2 > depth1) {
p2 = p2.getParent();
assert p2 != null;
if (p2.equals(first)) {
return AxisInfo.ANCESTOR;
}
depth2--;
}
// now delegate to compareOrder()
if (first.compareOrder(second) < 0) {
return AxisInfo.PRECEDING;
} else {
return AxisInfo.FOLLOWING;
}
}
/**
* Classify node kinds into categories for sorting into document order:
* 0 = document, 1 = namespace, 2 = attribute, 3 = (element, text, comment, pi)
*/
/*@NotNull*/ private static final int[] nodeCategories = {
-1, //0 = not used
3, //1 = element
2, //2 = attribute
3, //3 = text
-1, -1, -1, //4,5,6 = not used
3, //7 = processing-instruction
3, //8 = comment
0, //9 = document
-1, -1, -1, //10,11,12 = not used
1 //13 = namespace
};
/**
* Get a character string that uniquely identifies this node and that collates nodes
* into document order
*
* @param node the node whose unique identifier is reuqired
* @param sb a buffer to which the unique identifier will be appended
* @param addDocNr true if a unique document number is to be included in the information
*/
public static void appendSequentialKey(/*@NotNull*/ SiblingCountingNode node, /*@NotNull*/ StringBuilder sb, boolean addDocNr) {
if (addDocNr) {
sb.append('w');
sb.append(node.getTreeInfo().getDocumentNumber());
}
if (node.getNodeKind() != Type.DOCUMENT) {
NodeInfo parent = node.getParent();
if (parent != null) {
appendSequentialKey((SiblingCountingNode) parent, sb, false);
}
if (node.getNodeKind() == Type.ATTRIBUTE) {
sb.append('A');
}
}
sb.append(alphaKey(node.getSiblingPosition()));
}
/**
* Construct an alphabetic key from an positive integer; the key collates in the same sequence
* as the integer
*
* @param value The positive integer key value (negative values are treated as zero).
* @return the alphabetic key value
*/
/*@NotNull*/
public static String alphaKey(int value) {
if (value < 1) {
return "a";
}
if (value < 10) {
return "b" + value;
}
if (value < 100) {
return "c" + value;
}
if (value < 1000) {
return "d" + value;
}
if (value < 10000) {
return "e" + value;
}
if (value < 100000) {
return "f" + value;
}
if (value < 1000000) {
return "g" + value;
}
if (value < 10000000) {
return "h" + value;
}
if (value < 100000000) {
return "i" + value;
}
if (value < 1000000000) {
return "j" + value;
}
return "k" + value;
}
/**
* Test if one node is an ancestor-or-self of another
*
* @param a the putative ancestor-or-self node
* @param d the putative descendant node
* @return true if a is an ancestor-or-self of d
*/
public static boolean isAncestorOrSelf(/*@NotNull*/ NodeInfo a, /*@NotNull*/ NodeInfo d) {
int k = a.getNodeKind();
if (k != Type.ELEMENT && k != Type.DOCUMENT) {
return a.equals(d);
}
// Fast path for the TinyTree implementation
if (a instanceof TinyNodeImpl) {
if (d instanceof TinyNodeImpl) {
return ((TinyNodeImpl) a).isAncestorOrSelf((TinyNodeImpl) d);
} else if (d instanceof TinyTextualElement.TinyTextualElementText) {
return a.equals(d) || isAncestorOrSelf(a, d.getParent());
} else if (d.getNodeKind() == Type.NAMESPACE) {
// fall through
} else if (d instanceof VirtualCopy) {
// fall through
} else {
return false;
}
}
// Generic implementation
NodeInfo p = d;
while (p != null) {
if (a.equals(p)) {
return true;
}
p = p.getParent();
}
return false;
}
///////////////////////////////////////////////////////////////////////////////
// Helper classes to support axis iteration
///////////////////////////////////////////////////////////////////////////////
/**
* Convert a {@code Predicate} (if necessary) to a NodeTest
*
* @param predicate a Node predicate
* @return a NodeTest
*/
public static NodeTest nodeTestFromPredicate(NodePredicate predicate) {
if (predicate instanceof NodeTest) {
return (NodeTest)predicate;
} else {
return NodeSelector.of(predicate::test);
}
}
/**
* Create an iterator over a singleton node, if it exists and matches a nodetest;
* otherwise return an empty iterator
*
* @param node the singleton node, or null if the node does not exist
* @param nodeTest the test to be applied
* @return an iterator over the node if it exists and matches the test.
*/
/*@NotNull*/
public static AxisIterator filteredSingleton(NodeInfo node,
NodePredicate nodeTest) {
if (node != null && nodeTest.test(node)) {
return SingleNodeIterator.makeIterator(node);
} else {
return EmptyIterator.ofNodes();
}
}
/**
* Get the sibling position of a node: specifically, count how many preceding siblings
* of a node satisfy the nodetest.
*
* @param node the starting node, which is assumed to satisfy the node test
* @param nodeTest the node test
* @param max the maximum number of nodes to be counted
* @return the number of preceding siblings that satisfy the node test, plus one, unless the
* number exceeds max, in which case return some number greater than or equal to max.
*/
public static int getSiblingPosition(NodeInfo node, NodeTest nodeTest, int max) {
if (node instanceof SiblingCountingNode && nodeTest instanceof AnyNodeTest) {
return ((SiblingCountingNode)node).getSiblingPosition();
}
AxisIterator prev = node.iterateAxis(AxisInfo.PRECEDING_SIBLING, nodeTest);
int count = 1;
while (prev.next() != null) {
if (++count > max) {
return count;
}
}
return count;
}
/**
* A class that delivers the children of a node as a Java Iterable
*/
public static class ChildrenAsIterable implements Iterable {
private final NodeInfo parent;
private NodePredicate filter = null;
public ChildrenAsIterable(NodeInfo parent) {
this.parent = parent;
}
public ChildrenAsIterable(NodeInfo parent, NodePredicate filter) {
this.parent = parent;
this.filter = filter;
}
@Override
public Iterator iterator() {
AxisIterator basis;
if (filter == null) {
basis = parent.iterateAxis(AxisInfo.CHILD);
} else {
basis = parent.iterateAxis(AxisInfo.CHILD, filter);
}
return new WrappingJavaIterator(basis);
}
}
/**
* AxisFilter is an iterator that applies a NodeTest filter to
* the nodes returned by an underlying AxisIterator.
*/
public static class AxisFilter implements AxisIterator {
private final AxisIterator base;
private final NodePredicate nodeTest;
/**
* Construct a AxisFilter
*
* @param base the underlying iterator that returns all the nodes on
* a required axis. This must not be an atomizing iterator!
* @param test a NodeTest that is applied to each node returned by the
* underlying AxisIterator; only those nodes that pass the NodeTest are
* returned by the AxisFilter
*/
public AxisFilter(AxisIterator base, NodePredicate test) {
this.base = base;
nodeTest = test;
}
/*@Nullable*/
@Override
public NodeInfo next() {
while (true) {
NodeInfo next = base.next();
if (next == null) {
return null;
}
if (nodeTest.test(next)) {
return next;
}
}
}
}
/**
* EmptyTextFilter is an iterator that applies removes any zero-length text
* nodes returned by an underlying AxisIterator.
*/
public static class EmptyTextFilter implements AxisIterator {
private final AxisIterator base;
/**
* Construct an EmptyTextFilter
*
* @param base the underlying iterator that returns all the nodes on
* a required axis. This must not be an atomizing iterator
*/
public EmptyTextFilter(AxisIterator base) {
this.base = base;
}
/*@Nullable*/
@Override
public NodeInfo next() {
while (true) {
NodeInfo next = base.next();
if (next == null) {
return null;
}
if (!(next.getNodeKind() == Type.TEXT && next.getUnicodeStringValue().isEmpty())) {
return next;
}
}
}
}
/**
* General-purpose implementation of the ancestor and ancestor-or-self axes
*/
public static final class AncestorEnumeration implements AxisIterator {
private final boolean includeSelf;
private boolean atStart;
private NodeInfo current;
/**
* Create an iterator over the ancestor or ancestor-or-self axis
*
* @param start the initial context node
* @param includeSelf true if the "self" node is to be included
*/
public AncestorEnumeration(NodeInfo start, boolean includeSelf) {
this.includeSelf = includeSelf;
current = start;
atStart = true;
}
@Override
public final NodeInfo next() {
if (atStart) {
atStart = false;
if (includeSelf) {
return current;
}
}
return current = current == null ? null : current.getParent();
}
} // end of class AncestorEnumeration
/**
* General-purpose implementation of the descendant and descendant-or-self axes,
* in terms of the child axis.
* But it also has the option to return the descendants in reverse document order;
* this is used when evaluating the preceding axis. Note that the includeSelf option
* should not be used when scanning in reverse order, as the self node will always be
* returned first.
*/
public static final class DescendantEnumeration implements AxisIterator {
private SequenceIterator children = null;
private AxisIterator descendants = null;
private final NodeInfo start;
private final boolean includeSelf;
private final boolean forwards;
private boolean atEnd = false;
/**
* Create an iterator over the descendant or descendant-or-self axis
*
* @param start the initial context node
* @param includeSelf true if the "self" node is to be included
* @param forwards true for a forwards iteration, false for reverse order
*/
public DescendantEnumeration(NodeInfo start,
boolean includeSelf, boolean forwards) {
this.start = start;
this.includeSelf = includeSelf;
this.forwards = forwards;
}
@Override
public final NodeInfo next() {
if (descendants != null) {
NodeInfo nextd = descendants.next();
if (nextd != null) {
return nextd;
} else {
descendants = null;
}
}
if (children != null) {
NodeInfo n = (NodeInfo)children.next();
if (n != null) {
if (n.hasChildNodes()) {
if (forwards) {
descendants = new DescendantEnumeration(n, false, true);
return n;
} else {
descendants = new DescendantEnumeration(n, true, false);
return next();
}
} else {
return n;
}
} else {
if (forwards || !includeSelf) {
return null;
} else {
atEnd = true;
children = null;
return start;
}
}
} else if (atEnd) {
// we're just finishing a backwards scan
return null;
} else {
// we're just starting...
if (start.hasChildNodes()) {
children = start.iterateAxis(AxisInfo.CHILD);
if (!forwards) {
children = Reverse.getReverseIterator(children);
}
} else {
children = EmptyIterator.ofNodes();
}
if (forwards && includeSelf) {
return start;
} else {
return next();
}
}
}
public void advance() {
}
} // end of class DescendantEnumeration
/**
* General purpose implementation of the following axis, in terms of the
* ancestor, child, and following-sibling axes
*/
public static final class FollowingEnumeration implements AxisIterator {
/*@NotNull*/ private final AxisIterator ancestorEnum;
/*@Nullable*/ private AxisIterator siblingEnum;
/*@Nullable*/ private AxisIterator descendEnum = null;
/**
* Create an iterator over the "following" axis
*
* @param start the initial context node
*/
public FollowingEnumeration(/*@NotNull*/ NodeInfo start) {
ancestorEnum = new AncestorEnumeration(start, false);
switch (start.getNodeKind()) {
case Type.ELEMENT:
case Type.TEXT:
case Type.COMMENT:
case Type.PROCESSING_INSTRUCTION:
//siblingEnum = new NodeWrapper.ChildEnumeration(start, false, true);
// gets following siblings
siblingEnum = start.iterateAxis(AxisInfo.FOLLOWING_SIBLING);
break;
case Type.ATTRIBUTE:
case Type.NAMESPACE:
//siblingEnum = new NodeWrapper.ChildEnumeration((NodeWrapper)start.getParent(), true, true);
// gets children of the attribute's parent node
NodeInfo parent = start.getParent();
if (parent == null) {
siblingEnum = EmptyIterator.ofNodes();
} else {
siblingEnum = parent.iterateAxis(AxisInfo.CHILD);
}
break;
default:
siblingEnum = EmptyIterator.ofNodes();
break;
}
}
@Override
public final NodeInfo next() {
if (descendEnum != null) {
NodeInfo nextd = descendEnum.next();
if (nextd != null) {
return nextd;
} else {
descendEnum = null;
}
}
if (siblingEnum != null) {
NodeInfo nexts = siblingEnum.next();
if (nexts != null) {
if (nexts.hasChildNodes()) {
descendEnum = new DescendantEnumeration(nexts, false, true);
} else {
descendEnum = null;
}
return nexts;
} else {
descendEnum = null;
siblingEnum = null;
}
}
NodeInfo nexta = ancestorEnum.next();
if (nexta != null) {
if (nexta.getNodeKind() == Type.DOCUMENT) {
siblingEnum = EmptyIterator.ofNodes();
} else {
//siblingEnum = new NodeWrapper.ChildEnumeration(next, false, true);
siblingEnum = nexta.iterateAxis(AxisInfo.FOLLOWING_SIBLING);
}
return next();
} else {
return null;
}
}
} // end of class FollowingEnumeration
/**
* Helper method to iterate over the preceding axis, or Saxon's internal
* preceding-or-ancestor axis, by making use of the ancestor, descendant, and
* preceding-sibling axes.
*/
public static final class PrecedingEnumeration implements AxisIterator {
/*@NotNull*/ private final AxisIterator ancestorEnum;
/*@Nullable*/ private AxisIterator siblingEnum;
/*@Nullable*/ private AxisIterator descendEnum = null;
private final boolean includeAncestors;
/**
* Create an iterator for the preceding or "preceding-or-ancestor" axis (the latter being
* used internall to support xsl:number)
*
* @param start the initial context node
* @param includeAncestors true if ancestors of the initial context node are to be included
* in the result
*/
public PrecedingEnumeration(/*@NotNull*/ NodeInfo start, boolean includeAncestors) {
this.includeAncestors = includeAncestors;
ancestorEnum = new AncestorEnumeration(start, false);
switch (start.getNodeKind()) {
case Type.ELEMENT:
case Type.TEXT:
case Type.COMMENT:
case Type.PROCESSING_INSTRUCTION:
// get preceding-sibling enumeration
siblingEnum = start.iterateAxis(AxisInfo.PRECEDING_SIBLING);
break;
default:
siblingEnum = EmptyIterator.ofNodes();
break;
}
}
@Override
public final NodeInfo next() {
if (descendEnum != null) {
NodeInfo nextd = descendEnum.next();
if (nextd != null) {
return nextd;
} else {
descendEnum = null;
}
}
if (siblingEnum != null) {
NodeInfo nexts = siblingEnum.next();
if (nexts != null) {
if (nexts.hasChildNodes()) {
descendEnum = new DescendantEnumeration(nexts, true, false);
return next();
} else {
descendEnum = null;
return nexts;
}
} else {
descendEnum = null;
siblingEnum = null;
}
}
NodeInfo nexta = ancestorEnum.next();
if (nexta != null) {
if (nexta.getNodeKind() == Type.DOCUMENT) {
siblingEnum = EmptyIterator.ofNodes();
} else {
siblingEnum = nexta.iterateAxis(AxisInfo.PRECEDING_SIBLING);
}
if (!includeAncestors) {
return next();
} else {
return nexta;
}
} else {
return null;
}
}
} // end of class PrecedingEnumeration
}