package client.net.sf.saxon.ce.tree.wrapper; import client.net.sf.saxon.ce.event.Receiver; import client.net.sf.saxon.ce.event.Stripper; import client.net.sf.saxon.ce.lib.NamespaceConstant; import client.net.sf.saxon.ce.om.*; import client.net.sf.saxon.ce.pattern.NodeKindTest; import client.net.sf.saxon.ce.pattern.NodeTest; import client.net.sf.saxon.ce.trans.XPathException; import client.net.sf.saxon.ce.tree.iter.AxisIterator; import client.net.sf.saxon.ce.tree.iter.EmptyIterator; import client.net.sf.saxon.ce.tree.util.FastStringBuffer; import client.net.sf.saxon.ce.tree.util.Navigator; import client.net.sf.saxon.ce.type.Type; import client.net.sf.saxon.ce.value.*; import client.net.sf.saxon.ce.value.StringValue; /** * A StrippedNode is a view of a node, in a virtual tree that has whitespace * text nodes stripped from it. All operations on the node produce the same result * as operations on the real underlying node, except that iterations over the axes * take care to skip whitespace-only text nodes that are supposed to be stripped. * Note that this class is only used in cases where a pre-built tree is supplied as * the input to a transformation, and where the stylesheet does whitespace stripping; * if a SAXSource or StreamSource is supplied, whitespace is stripped as the tree * is built. */ public class SpaceStrippedNode extends AbstractVirtualNode implements WrappingFunction { protected SpaceStrippedNode() {} /** * This constructor is protected: nodes should be created using the makeWrapper * factory method * @param node The node to be wrapped * @param parent The StrippedNode that wraps the parent of this node */ protected SpaceStrippedNode(NodeInfo node, SpaceStrippedNode parent) { this.node = node; this.parent = parent; } /** * Factory method to wrap a node with a wrapper that implements the Saxon * NodeInfo interface. * @param node The underlying node * @param docWrapper The wrapper for the document node (must be supplied) * @param parent The wrapper for the parent of the node (null if unknown) * @return The new wrapper for the supplied node */ protected static SpaceStrippedNode makeWrapper(NodeInfo node, SpaceStrippedDocument docWrapper, SpaceStrippedNode parent) { SpaceStrippedNode wrapper = new SpaceStrippedNode(node, parent); wrapper.docWrapper = docWrapper; return wrapper; } /** * Factory method to wrap a node within the same document as this node with a VirtualNode * @param node The underlying node * @param parent The wrapper for the parent of the node (null if unknown) * @return The new wrapper for the supplied node */ public VirtualNode makeWrapper(NodeInfo node, VirtualNode parent) { SpaceStrippedNode wrapper = new SpaceStrippedNode(node, (SpaceStrippedNode)parent); wrapper.docWrapper = this.docWrapper; return wrapper; } /** * Get the typed value. The result of this method will always be consistent with the method * {@link client.net.sf.saxon.ce.om.Item#getTypedValue()}. However, this method is often more convenient and may be * more efficient, especially in the common case where the value is expected to be a singleton. * * @return the typed value. If requireSingleton is set to true, the result will always be an * AtomicValue. In other cases it may be a Value representing a sequence whose items are atomic * values. * @since 8.5 */ public AtomicValue getTypedValue() { // We rely on the fact that for all simple types other than string, whitespace is collapsed, // so the atomized value of the stripped node is the same as the atomized value of its underlying // node. Only when the simple type is string do we need to strip unwanted whitespace text nodes AtomicValue baseVal = node.getTypedValue(); if (baseVal instanceof StringValue) { int primitiveType = baseVal.getTypeLabel().getPrimitiveType(); switch (primitiveType) { case StandardNames.XS_STRING: return new StringValue(getStringValueCS()); case StandardNames.XS_ANY_URI: return new AnyURIValue(getStringValueCS()); default: return new UntypedAtomicValue(getStringValueCS()); } } else { return baseVal; } } /** * Determine whether this is the same node as another node. <br /> * Note: a.isSameNode(b) if and only if generateId(a)==generateId(b) * @return true if this Node object and the supplied Node object represent the * same node in the tree. */ public boolean isSameNodeInfo(NodeInfo other) { if (other instanceof SpaceStrippedNode) { return node.isSameNodeInfo(((SpaceStrippedNode)other).node); } else { return node.isSameNodeInfo(other); } } /** * Determine the relative position of this node and another node, in document order. * The other node will always be in the same document. * @param other The other node, whose position is to be compared with this 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 int compareOrder(NodeInfo other) { if (other instanceof SpaceStrippedNode) { return node.compareOrder(((SpaceStrippedNode)other).node); } else { return node.compareOrder(other); } } /** * Get the value of the item as a CharSequence. This is in some cases more efficient than * the version of the method that returns a String. */ public CharSequence getStringValueCS() { // Might not be the same as the string value of the underlying node because of space stripping switch (getNodeKind()) { case Type.DOCUMENT: case Type.ELEMENT: AxisIterator iter = iterateAxis(Axis.DESCENDANT, NodeKindTest.makeNodeKindTest(Type.TEXT)); FastStringBuffer sb = new FastStringBuffer(FastStringBuffer.SMALL); while(true) { NodeInfo it = (NodeInfo)iter.next(); if (it == null) { break; } sb.append(it.getStringValueCS()); } return sb.condense(); default: return node.getStringValueCS(); } } /** * Get the NodeInfo object representing the parent of this node */ public NodeInfo getParent() { if (parent==null) { NodeInfo realParent = node.getParent(); if (realParent != null) { parent = makeWrapper(realParent, (SpaceStrippedDocument)docWrapper, null); } } return parent; } /** * Return an iteration over the nodes reached by the given axis from this node * @param axisNumber the axis to be used * @return a SequenceIterator that scans the nodes reached by the axis in turn. */ public AxisIterator iterateAxis(byte axisNumber) { switch (axisNumber) { case Axis.ATTRIBUTE: case Axis.NAMESPACE: return new WrappingIterator(node.iterateAxis(axisNumber), this, this); case Axis.CHILD: return new StrippingIterator(node.iterateAxis(axisNumber), this); case Axis.FOLLOWING_SIBLING: case Axis.PRECEDING_SIBLING: SpaceStrippedNode parent = (SpaceStrippedNode)getParent(); if (parent == null) { return EmptyIterator.getInstance(); } else { return new StrippingIterator(node.iterateAxis(axisNumber), parent); } default: return new StrippingIterator(node.iterateAxis(axisNumber), null); } } /** * Copy this node to a given outputter (deep copy) */ public void copy(Receiver out, int copyOptions) throws XPathException { // The underlying code does not do whitespace stripping. So we need to interpose // a stripper. Stripper stripper = ((SpaceStrippedDocument)docWrapper).getStripper().getAnother(); stripper.setUnderlyingReceiver(out); node.copy(stripper, copyOptions); } /** * A StrippingIterator delivers wrappers for the nodes delivered * by its underlying iterator. It is used when whitespace stripping * may be needed, e.g. for the child axis. It examines all text nodes * encountered to see if they need to be stripped, and if so, it * skips them. */ private final class StrippingIterator implements AxisIterator { AxisIterator base; SpaceStrippedNode parent; NodeInfo currentVirtualNode; int position; /** * Create a StrippingIterator * @param base The underlying iterator * @param parent If all the nodes to be wrapped have the same parent, * it can be specified here. Otherwise specify null. */ public StrippingIterator(AxisIterator base, SpaceStrippedNode parent) { this.base = base; this.parent = parent; position = 0; } /** * Move to the next node, without returning it. Returns true if there is * a next node, false if the end of the sequence has been reached. After * calling this method, the current node may be retrieved using the * current() function. */ public boolean moveNext() { return (next() != null); } public Item next() { NodeInfo nextRealNode; while (true) { nextRealNode = (NodeInfo)base.next(); if (nextRealNode==null) { return null; } if (isPreserved(nextRealNode)) { break; } // otherwise skip this whitespace text node } currentVirtualNode = makeWrapper(nextRealNode,(SpaceStrippedDocument)docWrapper, parent); position++; return currentVirtualNode; } private boolean isPreserved(NodeInfo nextRealNode) { if (nextRealNode.getNodeKind() != Type.TEXT) { return true; } if (!Whitespace.isWhite(nextRealNode.getStringValueCS())) { return true; } NodeInfo actualParent = (parent==null ? nextRealNode.getParent() : parent.node); if (((SpaceStrippedDocument)docWrapper).containsPreserveSpace()) { NodeInfo p = actualParent; // the document contains one or more xml:space="preserve" attributes, so we need to see // if one of them is on an ancestor of this node while (p.getNodeKind() == Type.ELEMENT) { String val = Navigator.getAttributeValue(p, NamespaceConstant.XML, "space"); if (val != null) { if ("preserve".equals(val)) { return true; } else if ("default".equals(val)) { break; } } p = p.getParent(); } } try { byte preserve = ((SpaceStrippedDocument)docWrapper).getStripper().isSpacePreserving(actualParent.getFingerprint()); return preserve == Stripper.ALWAYS_PRESERVE; } catch (XPathException e) { // Ambiguity between strip-space and preserve-space. Because we're in an axis iterator, // we don't get an opportunity to fail, so take the recovery action. return true; } } public Item current() { return currentVirtualNode; } public int position() { return position; } /** * Return an iterator over an axis, starting at the current node. * * @param axis the axis to iterate over, using a constant such as * {@link Axis#CHILD} * @param test a predicate to apply to the nodes before returning them. * @throws NullPointerException if there is no current node */ public AxisIterator iterateAxis(byte axis, NodeTest test) { return currentVirtualNode.iterateAxis(axis, test); } /** * Return the atomized value of the current node. * * @return the atomized value. * @throws NullPointerException if there is no current node */ public Value atomize() throws XPathException { return currentVirtualNode.getTypedValue(); } /** * Return the string value of the current node. * * @return the string value, as an instance of CharSequence. * @throws NullPointerException if there is no current node */ public CharSequence getStringValue() { return currentVirtualNode.getStringValue(); } public SequenceIterator getAnother() { return new StrippingIterator((AxisIterator)base.getAnother(), parent); } /** * Get properties of this iterator, as a bit-significant integer. * * @return the properties of this iterator. This will be some combination of * properties such as {@link #GROUNDED}, {@link #LAST_POSITION_FINDER}, * and {@link #LOOKAHEAD}. It is always * acceptable to return the value zero, indicating that there are no known special properties. * It is acceptable for the properties of the iterator to change depending on its state. */ public int getProperties() { return 0; } } // end of class StrippingIterator } // 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.