001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.jxpath.ri.model.dom;
019
020import java.util.HashMap;
021import java.util.Locale;
022import java.util.Map;
023
024import org.apache.commons.jxpath.JXPathAbstractFactoryException;
025import org.apache.commons.jxpath.JXPathContext;
026import org.apache.commons.jxpath.JXPathException;
027import org.apache.commons.jxpath.Pointer;
028import org.apache.commons.jxpath.ri.Compiler;
029import org.apache.commons.jxpath.ri.NamespaceResolver;
030import org.apache.commons.jxpath.ri.QName;
031import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
032import org.apache.commons.jxpath.ri.compiler.NodeTest;
033import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
034import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest;
035import org.apache.commons.jxpath.ri.model.NodeIterator;
036import org.apache.commons.jxpath.ri.model.NodePointer;
037import org.apache.commons.jxpath.ri.model.beans.NullPointer;
038import org.apache.commons.jxpath.util.TypeUtils;
039import org.w3c.dom.Attr;
040import org.w3c.dom.Comment;
041import org.w3c.dom.Document;
042import org.w3c.dom.Element;
043import org.w3c.dom.NamedNodeMap;
044import org.w3c.dom.Node;
045import org.w3c.dom.NodeList;
046import org.w3c.dom.ProcessingInstruction;
047
048/**
049 * A Pointer that points to a DOM node. Because a DOM Node is not guaranteed Serializable, a DOMNodePointer instance may likewise not be properly Serializable.
050 */
051public class DOMNodePointer extends NodePointer {
052
053    private static final long serialVersionUID = -8751046933894857319L;
054    /** XML namespace URI */
055    public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
056    /** XMLNS namespace URI */
057    public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
058
059    /**
060     * Test string equality.
061     *
062     * @param s1 String 1
063     * @param s2 String 2
064     * @return true if == or .equals()
065     */
066    private static boolean equalStrings(String s1, String s2) {
067        if (s1 == s2) {
068            return true;
069        }
070        s1 = s1 == null ? "" : s1.trim();
071        s2 = s2 == null ? "" : s2.trim();
072        return s1.equals(s2);
073    }
074
075    /**
076     * Find the nearest occurrence of the specified attribute on the specified and enclosing elements.
077     *
078     * @param n        current node
079     * @param attrName attribute name
080     * @return attribute value
081     */
082    protected static String findEnclosingAttribute(Node n, final String attrName) {
083        while (n != null) {
084            if (n.getNodeType() == Node.ELEMENT_NODE) {
085                final Element e = (Element) n;
086                final String attr = e.getAttribute(attrName);
087                if (attr != null && !attr.isEmpty()) {
088                    return attr;
089                }
090            }
091            n = n.getParentNode();
092        }
093        return null;
094    }
095
096    /**
097     * Gets the local name of the specified node.
098     *
099     * @param node node to check
100     * @return String local name
101     */
102    public static String getLocalName(final Node node) {
103        final String localName = node.getLocalName();
104        if (localName != null) {
105            return localName;
106        }
107        final String name = node.getNodeName();
108        final int index = name.lastIndexOf(':');
109        return index < 0 ? name : name.substring(index + 1);
110    }
111
112    /**
113     * Gets the ns uri of the specified node.
114     *
115     * @param node Node to check
116     * @return String ns uri
117     */
118    public static String getNamespaceURI(Node node) {
119        if (node instanceof Document) {
120            node = ((Document) node).getDocumentElement();
121        }
122        final Element element = (Element) node;
123        String uri = element.getNamespaceURI();
124        if (uri == null) {
125            final String prefix = getPrefix(node);
126            final String qname = prefix == null ? "xmlns" : "xmlns:" + prefix;
127            Node aNode = node;
128            while (aNode != null) {
129                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
130                    final Attr attr = ((Element) aNode).getAttributeNode(qname);
131                    if (attr != null) {
132                        uri = attr.getValue();
133                        break;
134                    }
135                }
136                aNode = aNode.getParentNode();
137            }
138        }
139        return "".equals(uri) ? null : uri;
140    }
141
142    /**
143     * Gets any prefix from the specified node.
144     *
145     * @param node the node to check
146     * @return String xml prefix
147     */
148    public static String getPrefix(final Node node) {
149        final String prefix = node.getPrefix();
150        if (prefix != null) {
151            return prefix;
152        }
153        final String name = node.getNodeName();
154        final int index = name.lastIndexOf(':');
155        return index < 0 ? null : name.substring(0, index);
156    }
157
158    /**
159     * Test a Node.
160     *
161     * @param node to test
162     * @param test to execute
163     * @return true if node passes test
164     */
165    public static boolean testNode(final Node node, final NodeTest test) {
166        if (test == null) {
167            return true;
168        }
169        if (test instanceof NodeNameTest) {
170            if (node.getNodeType() != Node.ELEMENT_NODE) {
171                return false;
172            }
173            final NodeNameTest nodeNameTest = (NodeNameTest) test;
174            final QName testName = nodeNameTest.getNodeName();
175            final String namespaceURI = nodeNameTest.getNamespaceURI();
176            final boolean wildcard = nodeNameTest.isWildcard();
177            final String testPrefix = testName.getPrefix();
178            if (wildcard && testPrefix == null) {
179                return true;
180            }
181            if (wildcard || testName.getName().equals(getLocalName(node))) {
182                final String nodeNS = getNamespaceURI(node);
183                return equalStrings(namespaceURI, nodeNS) || nodeNS == null && equalStrings(testPrefix, getPrefix(node));
184            }
185            return false;
186        }
187        if (test instanceof NodeTypeTest) {
188            final int nodeType = node.getNodeType();
189            switch (((NodeTypeTest) test).getNodeType()) {
190            case Compiler.NODE_TYPE_NODE:
191                return true;
192            case Compiler.NODE_TYPE_TEXT:
193                return nodeType == Node.CDATA_SECTION_NODE || nodeType == Node.TEXT_NODE;
194            case Compiler.NODE_TYPE_COMMENT:
195                return nodeType == Node.COMMENT_NODE;
196            case Compiler.NODE_TYPE_PI:
197                return nodeType == Node.PROCESSING_INSTRUCTION_NODE;
198            default:
199                return false;
200            }
201        }
202        if (test instanceof ProcessingInstructionTest && node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
203            final String testPI = ((ProcessingInstructionTest) test).getTarget();
204            final String nodePI = ((ProcessingInstruction) node).getTarget();
205            return testPI.equals(nodePI);
206        }
207        return false;
208    }
209
210    /**
211     * A DOM node supporting {@link #getImmediateNode()}.
212     */
213    private final Node node;
214
215    /**
216     * Supports {@link #getDefaultNamespaceURI()}.
217     */
218    private Map<String, String> namespaces;
219
220    /**
221     * Supports {@link #getNamespaceURI(String)}.
222     */
223    private String defaultNamespace;
224
225    /**
226     * Optional ID.
227     */
228    private final String id;
229
230    /**
231     * Supports {@link #getNamespaceResolver()}.
232     */
233    private NamespaceResolver localNamespaceResolver;
234
235    /**
236     * Constructs a new DOMNodePointer.
237     *
238     * @param node   A node.
239     * @param locale Locale.
240     */
241    public DOMNodePointer(final Node node, final Locale locale) {
242        this(node, locale, null);
243    }
244
245    /**
246     * Constructs a new DOMNodePointer.
247     *
248     * @param node   A node.
249     * @param locale Locale.
250     * @param id     String ID.
251     */
252    public DOMNodePointer(final Node node, final Locale locale, final String id) {
253        super(null, locale);
254        this.node = node;
255        this.id = id;
256    }
257
258    /**
259     * Constructs a new DOMNodePointer.
260     *
261     * @param parent pointer
262     * @param node   pointed
263     */
264    public DOMNodePointer(final NodePointer parent, final Node node) {
265        super(parent);
266        this.node = node;
267        this.id = null;
268    }
269
270    @Override
271    public String asPath() {
272        if (id != null) {
273            return "id('" + escape(id) + "')";
274        }
275        final StringBuilder buffer = new StringBuilder();
276        if (parent != null) {
277            buffer.append(parent.asPath());
278        }
279        switch (node.getNodeType()) {
280        case Node.ELEMENT_NODE:
281            // If the parent pointer is not a DOMNodePointer, it is
282            // the parent's responsibility to produce the node test part
283            // of the path
284            if (parent instanceof DOMNodePointer) {
285                if (buffer.length() == 0 || buffer.charAt(buffer.length() - 1) != '/') {
286                    buffer.append('/');
287                }
288                final String ln = getLocalName(node);
289                final String nsURI = getNamespaceURI();
290                if (nsURI == null) {
291                    buffer.append(ln);
292                    buffer.append('[');
293                    buffer.append(getRelativePositionByQName()).append(']');
294                } else {
295                    final String prefix = getNamespaceResolver().getPrefix(nsURI);
296                    if (prefix != null) {
297                        buffer.append(prefix);
298                        buffer.append(':');
299                        buffer.append(ln);
300                        buffer.append('[');
301                        buffer.append(getRelativePositionByQName());
302                    } else {
303                        buffer.append("node()");
304                        buffer.append('[');
305                        buffer.append(getRelativePositionOfElement());
306                    }
307                    buffer.append(']');
308                }
309            }
310            break;
311        case Node.TEXT_NODE:
312        case Node.CDATA_SECTION_NODE:
313            buffer.append("/text()");
314            buffer.append('[');
315            buffer.append(getRelativePositionOfTextNode()).append(']');
316            break;
317        case Node.PROCESSING_INSTRUCTION_NODE:
318            buffer.append("/processing-instruction(\'");
319            buffer.append(((ProcessingInstruction) node).getTarget()).append("')");
320            buffer.append('[');
321            buffer.append(getRelativePositionOfPI()).append(']');
322            break;
323        case Node.DOCUMENT_NODE:
324            // That'll be empty
325            break;
326        default:
327            break;
328        }
329        return buffer.toString();
330    }
331
332    @Override
333    public NodeIterator attributeIterator(final QName qName) {
334        return new DOMAttributeIterator(this, qName);
335    }
336
337    @Override
338    public NodeIterator childIterator(final NodeTest test, final boolean reverse, final NodePointer startWith) {
339        return new DOMNodeIterator(this, test, reverse, startWith);
340    }
341
342    @Override
343    public int compareChildNodePointers(final NodePointer pointer1, final NodePointer pointer2) {
344        final Node node1 = (Node) pointer1.getBaseValue();
345        final Node node2 = (Node) pointer2.getBaseValue();
346        if (node1 == node2) {
347            return 0;
348        }
349        final int t1 = node1.getNodeType();
350        final int t2 = node2.getNodeType();
351        if (t1 == Node.ATTRIBUTE_NODE && t2 != Node.ATTRIBUTE_NODE) {
352            return -1;
353        }
354        if (t1 != Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) {
355            return 1;
356        }
357        if (t1 == Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) {
358            final NamedNodeMap map = ((Node) getNode()).getAttributes();
359            final int length = map.getLength();
360            for (int i = 0; i < length; i++) {
361                final Node n = map.item(i);
362                if (n == node1) {
363                    return -1;
364                }
365                if (n == node2) {
366                    return 1;
367                }
368            }
369            return 0; // Should not happen
370        }
371        Node current = node.getFirstChild();
372        while (current != null) {
373            if (current == node1) {
374                return -1;
375            }
376            if (current == node2) {
377                return 1;
378            }
379            current = current.getNextSibling();
380        }
381        return 0;
382    }
383
384    @Override
385    public NodePointer createAttribute(final JXPathContext context, final QName qName) {
386        if (!(node instanceof Element)) {
387            return super.createAttribute(context, qName);
388        }
389        final Element element = (Element) node;
390        final String prefix = qName.getPrefix();
391        if (prefix != null) {
392            String ns = null;
393            final NamespaceResolver nsr = getNamespaceResolver();
394            if (nsr != null) {
395                ns = nsr.getNamespaceURI(prefix);
396            }
397            if (ns == null) {
398                throw new JXPathException("Unknown namespace prefix: " + prefix);
399            }
400            element.setAttributeNS(ns, qName.toString(), "");
401        } else if (!element.hasAttribute(qName.getName())) {
402            element.setAttribute(qName.getName(), "");
403        }
404        final NodeIterator it = attributeIterator(qName);
405        it.setPosition(1);
406        return it.getNodePointer();
407    }
408
409    @Override
410    public NodePointer createChild(final JXPathContext context, final QName qName, int index) {
411        if (index == WHOLE_COLLECTION) {
412            index = 0;
413        }
414        final boolean success = getAbstractFactory(context).createObject(context, this, node, qName.toString(), index);
415        if (success) {
416            NodeTest nodeTest;
417            final String prefix = qName.getPrefix();
418            final String namespaceURI = prefix == null ? null : context.getNamespaceURI(prefix);
419            nodeTest = new NodeNameTest(qName, namespaceURI);
420            final NodeIterator it = childIterator(nodeTest, false, null);
421            if (it != null && it.setPosition(index + 1)) {
422                return it.getNodePointer();
423            }
424        }
425        throw new JXPathAbstractFactoryException("Factory could not create a child node for path: " + asPath() + "/" + qName + "[" + (index + 1) + "]");
426    }
427
428    @Override
429    public NodePointer createChild(final JXPathContext context, final QName qName, final int index, final Object value) {
430        final NodePointer ptr = createChild(context, qName, index);
431        ptr.setValue(value);
432        return ptr;
433    }
434
435    @Override
436    public boolean equals(final Object object) {
437        return object == this || object instanceof DOMNodePointer && node == ((DOMNodePointer) object).node;
438    }
439
440    @Override
441    public Object getBaseValue() {
442        return node;
443    }
444
445    @Override
446    public String getDefaultNamespaceURI() {
447        if (defaultNamespace == null) {
448            Node aNode = node;
449            if (aNode instanceof Document) {
450                aNode = ((Document) aNode).getDocumentElement();
451            }
452            while (aNode != null) {
453                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
454                    final Attr attr = ((Element) aNode).getAttributeNode("xmlns");
455                    if (attr != null) {
456                        defaultNamespace = attr.getValue();
457                        break;
458                    }
459                }
460                aNode = aNode.getParentNode();
461            }
462        }
463        if (defaultNamespace == null) {
464            defaultNamespace = "";
465        }
466        // TBD: We are supposed to resolve relative URIs to absolute ones.
467        return defaultNamespace.isEmpty() ? null : defaultNamespace;
468    }
469
470    @Override
471    public Object getImmediateNode() {
472        return node;
473    }
474
475    /**
476     * Gets the language attribute for this node.
477     *
478     * @return String language name
479     */
480    protected String getLanguage() {
481        return findEnclosingAttribute(node, "xml:lang");
482    }
483
484    @Override
485    public int getLength() {
486        return 1;
487    }
488
489    @Override
490    public QName getName() {
491        String ln = null;
492        String ns = null;
493        final int type = node.getNodeType();
494        if (type == Node.ELEMENT_NODE) {
495            ns = getPrefix(node);
496            ln = getLocalName(node);
497        } else if (type == Node.PROCESSING_INSTRUCTION_NODE) {
498            ln = ((ProcessingInstruction) node).getTarget();
499        }
500        return new QName(ns, ln);
501    }
502
503    @Override
504    public synchronized NamespaceResolver getNamespaceResolver() {
505        if (localNamespaceResolver == null) {
506            localNamespaceResolver = new NamespaceResolver(super.getNamespaceResolver());
507            localNamespaceResolver.setNamespaceContextPointer(this);
508        }
509        return localNamespaceResolver;
510    }
511
512    @Override
513    public String getNamespaceURI() {
514        return getNamespaceURI(node);
515    }
516
517    @Override
518    public String getNamespaceURI(final String prefix) {
519        if (prefix == null || prefix.isEmpty()) {
520            return getDefaultNamespaceURI();
521        }
522        if (prefix.equals("xml")) {
523            return XML_NAMESPACE_URI;
524        }
525        if (prefix.equals("xmlns")) {
526            return XMLNS_NAMESPACE_URI;
527        }
528        String namespace = null;
529        if (namespaces == null) {
530            namespaces = new HashMap<>();
531        } else {
532            namespace = namespaces.get(prefix);
533        }
534        if (namespace == null) {
535            final String qname = "xmlns:" + prefix;
536            Node aNode = node;
537            if (aNode instanceof Document) {
538                aNode = ((Document) aNode).getDocumentElement();
539            }
540            while (aNode != null) {
541                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
542                    final Attr attr = ((Element) aNode).getAttributeNode(qname);
543                    if (attr != null) {
544                        namespace = attr.getValue();
545                        break;
546                    }
547                }
548                aNode = aNode.getParentNode();
549            }
550            if (namespace == null || namespace.isEmpty()) {
551                namespace = UNKNOWN_NAMESPACE;
552            }
553        }
554        namespaces.put(prefix, namespace);
555        if (namespace == UNKNOWN_NAMESPACE) {
556            return null;
557        }
558        // TBD: We are supposed to resolve relative URIs to absolute ones.
559        return namespace;
560    }
561
562    /**
563     * Locates a node by ID.
564     *
565     * @param context starting context
566     * @param id      to find
567     * @return Pointer
568     */
569    @Override
570    public Pointer getPointerByID(final JXPathContext context, final String id) {
571        final Document document = node.getNodeType() == Node.DOCUMENT_NODE ? (Document) node : node.getOwnerDocument();
572        final Element element = document.getElementById(id);
573        return element == null ? (Pointer) new NullPointer(getLocale(), id) : new DOMNodePointer(element, getLocale(), id);
574    }
575
576    /**
577     * Gets relative position of this among like-named siblings.
578     *
579     * @return 1..n
580     */
581    private int getRelativePositionByQName() {
582        int count = 1;
583        Node n = node.getPreviousSibling();
584        while (n != null) {
585            if (n.getNodeType() == Node.ELEMENT_NODE && matchesQName(n)) {
586                count++;
587            }
588            n = n.getPreviousSibling();
589        }
590        return count;
591    }
592
593    /**
594     * Gets relative position of this among all siblings.
595     *
596     * @return 1..n
597     */
598    private int getRelativePositionOfElement() {
599        int count = 1;
600        Node n = node.getPreviousSibling();
601        while (n != null) {
602            if (n.getNodeType() == Node.ELEMENT_NODE) {
603                count++;
604            }
605            n = n.getPreviousSibling();
606        }
607        return count;
608    }
609
610    /**
611     * Gets the relative position of this among same-target processing instruction siblings.
612     *
613     * @return 1..n
614     */
615    private int getRelativePositionOfPI() {
616        int count = 1;
617        final String target = ((ProcessingInstruction) node).getTarget();
618        Node n = node.getPreviousSibling();
619        while (n != null) {
620            if (n.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE && ((ProcessingInstruction) n).getTarget().equals(target)) {
621                count++;
622            }
623            n = n.getPreviousSibling();
624        }
625        return count;
626    }
627
628    /**
629     * Gets the relative position of this among sibling text nodes.
630     *
631     * @return 1..n
632     */
633    private int getRelativePositionOfTextNode() {
634        int count = 1;
635        Node n = node.getPreviousSibling();
636        while (n != null) {
637            if (n.getNodeType() == Node.TEXT_NODE || n.getNodeType() == Node.CDATA_SECTION_NODE) {
638                count++;
639            }
640            n = n.getPreviousSibling();
641        }
642        return count;
643    }
644
645    @Override
646    public Object getValue() {
647        if (node.getNodeType() == Node.COMMENT_NODE) {
648            final String text = ((Comment) node).getData();
649            return text == null ? "" : text.trim();
650        }
651        return stringValue(node);
652    }
653
654    @Override
655    public int hashCode() {
656        return node.hashCode();
657    }
658
659    @Override
660    public boolean isActual() {
661        return true;
662    }
663
664    @Override
665    public boolean isCollection() {
666        return false;
667    }
668
669    /**
670     * Returns true if the xml:lang attribute for the current node or its parent has the specified prefix <em>lang</em>. If no node has this prefix, calls
671     * {@code super.isLanguage(lang)}.
672     *
673     * @param lang ns to test
674     * @return boolean
675     */
676    @Override
677    public boolean isLanguage(final String lang) {
678        final String current = getLanguage();
679        return current == null ? super.isLanguage(lang) : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH));
680    }
681
682    @Override
683    public boolean isLeaf() {
684        return !node.hasChildNodes();
685    }
686
687    private boolean matchesQName(final Node n) {
688        if (getNamespaceURI() != null) {
689            return equalStrings(getNamespaceURI(n), getNamespaceURI()) && equalStrings(node.getLocalName(), n.getLocalName());
690        }
691        return equalStrings(node.getNodeName(), n.getNodeName());
692    }
693
694    @Override
695    public NodeIterator namespaceIterator() {
696        return new DOMNamespaceIterator(this);
697    }
698
699    @Override
700    public NodePointer namespacePointer(final String prefix) {
701        return new NamespacePointer(this, prefix);
702    }
703
704    @Override
705    public void remove() {
706        final Node parent = node.getParentNode();
707        if (parent == null) {
708            throw new JXPathException("Cannot remove root DOM node");
709        }
710        parent.removeChild(node);
711    }
712
713    /**
714     * Sets contents of the node to the specified value. If the value is a String, the contents of the node are replaced with this text. If the value is an
715     * Element or Document, the children of the node are replaced with the children of the passed node.
716     *
717     * @param value to set
718     */
719    @Override
720    public void setValue(final Object value) {
721        if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE) {
722            final String string = (String) TypeUtils.convert(value, String.class);
723            if (string != null && !string.isEmpty()) {
724                node.setNodeValue(string);
725            } else {
726                node.getParentNode().removeChild(node);
727            }
728        } else {
729            NodeList children = node.getChildNodes();
730            final int count = children.getLength();
731            for (int i = count; --i >= 0;) {
732                final Node child = children.item(i);
733                node.removeChild(child);
734            }
735            if (value instanceof Node) {
736                final Node valueNode = (Node) value;
737                if (valueNode instanceof Element || valueNode instanceof Document) {
738                    children = valueNode.getChildNodes();
739                    for (int i = 0; i < children.getLength(); i++) {
740                        final Node child = children.item(i);
741                        node.appendChild(child.cloneNode(true));
742                    }
743                } else {
744                    node.appendChild(valueNode.cloneNode(true));
745                }
746            } else {
747                final String string = (String) TypeUtils.convert(value, String.class);
748                if (string != null && !string.isEmpty()) {
749                    final Node textNode = node.getOwnerDocument().createTextNode(string);
750                    node.appendChild(textNode);
751                }
752            }
753        }
754    }
755
756    /**
757     * Gets the string value of the specified node.
758     *
759     * @param node Node to check
760     * @return String
761     */
762    private String stringValue(final Node node) {
763        final int nodeType = node.getNodeType();
764        if (nodeType == Node.COMMENT_NODE) {
765            return "";
766        }
767        final boolean trim = !"preserve".equals(findEnclosingAttribute(node, "xml:space"));
768        if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
769            final String text = node.getNodeValue();
770            return text == null ? "" : trim ? text.trim() : text;
771        }
772        if (nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
773            final String text = ((ProcessingInstruction) node).getData();
774            return text == null ? "" : trim ? text.trim() : text;
775        }
776        final NodeList list = node.getChildNodes();
777        final StringBuilder buf = new StringBuilder();
778        for (int i = 0; i < list.getLength(); i++) {
779            final Node child = list.item(i);
780            buf.append(stringValue(child));
781        }
782        return buf.toString();
783    }
784
785    @Override
786    public boolean testNode(final NodeTest test) {
787        return testNode(node, test);
788    }
789}