View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.jxpath.ri.model.dom;
19  
20  import java.util.HashMap;
21  import java.util.Locale;
22  import java.util.Map;
23  
24  import org.apache.commons.jxpath.JXPathAbstractFactoryException;
25  import org.apache.commons.jxpath.JXPathContext;
26  import org.apache.commons.jxpath.JXPathException;
27  import org.apache.commons.jxpath.Pointer;
28  import org.apache.commons.jxpath.ri.Compiler;
29  import org.apache.commons.jxpath.ri.NamespaceResolver;
30  import org.apache.commons.jxpath.ri.QName;
31  import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
32  import org.apache.commons.jxpath.ri.compiler.NodeTest;
33  import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
34  import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest;
35  import org.apache.commons.jxpath.ri.model.NodeIterator;
36  import org.apache.commons.jxpath.ri.model.NodePointer;
37  import org.apache.commons.jxpath.ri.model.beans.NullPointer;
38  import org.apache.commons.jxpath.util.TypeUtils;
39  import org.w3c.dom.Attr;
40  import org.w3c.dom.Comment;
41  import org.w3c.dom.Document;
42  import org.w3c.dom.Element;
43  import org.w3c.dom.NamedNodeMap;
44  import org.w3c.dom.Node;
45  import org.w3c.dom.NodeList;
46  import org.w3c.dom.ProcessingInstruction;
47  
48  /**
49   * 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.
50   */
51  public class DOMNodePointer extends NodePointer {
52  
53      private static final long serialVersionUID = -8751046933894857319L;
54      /** XML namespace URI */
55      public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
56      /** XMLNS namespace URI */
57      public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
58  
59      /**
60       * Test string equality.
61       *
62       * @param s1 String 1
63       * @param s2 String 2
64       * @return true if == or .equals()
65       */
66      private static boolean equalStrings(String s1, String s2) {
67          if (s1 == s2) {
68              return true;
69          }
70          s1 = s1 == null ? "" : s1.trim();
71          s2 = s2 == null ? "" : s2.trim();
72          return s1.equals(s2);
73      }
74  
75      /**
76       * Find the nearest occurrence of the specified attribute on the specified and enclosing elements.
77       *
78       * @param n        current node
79       * @param attrName attribute name
80       * @return attribute value
81       */
82      protected static String findEnclosingAttribute(Node n, final String attrName) {
83          while (n != null) {
84              if (n.getNodeType() == Node.ELEMENT_NODE) {
85                  final Element e = (Element) n;
86                  final String attr = e.getAttribute(attrName);
87                  if (attr != null && !attr.isEmpty()) {
88                      return attr;
89                  }
90              }
91              n = n.getParentNode();
92          }
93          return null;
94      }
95  
96      /**
97       * Gets the local name of the specified node.
98       *
99       * @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 }