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.jdom;
19  
20  import java.util.List;
21  import java.util.Locale;
22  
23  import org.apache.commons.jxpath.JXPathAbstractFactoryException;
24  import org.apache.commons.jxpath.JXPathContext;
25  import org.apache.commons.jxpath.JXPathException;
26  import org.apache.commons.jxpath.ri.Compiler;
27  import org.apache.commons.jxpath.ri.NamespaceResolver;
28  import org.apache.commons.jxpath.ri.QName;
29  import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
30  import org.apache.commons.jxpath.ri.compiler.NodeTest;
31  import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
32  import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest;
33  import org.apache.commons.jxpath.ri.model.NodeIterator;
34  import org.apache.commons.jxpath.ri.model.NodePointer;
35  import org.apache.commons.jxpath.util.TypeUtils;
36  import org.jdom.Attribute;
37  import org.jdom.CDATA;
38  import org.jdom.Comment;
39  import org.jdom.Document;
40  import org.jdom.Element;
41  import org.jdom.Namespace;
42  import org.jdom.ProcessingInstruction;
43  import org.jdom.Text;
44  
45  /**
46   * A Pointer that points to a DOM node.
47   */
48  public class JDOMNodePointer extends NodePointer {
49  
50      private static final long serialVersionUID = -6346532297491082651L;
51      /** XML ns uri */
52      public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
53      /** XMLNS ns uri */
54      public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
55  
56      /**
57       * Tests whether two strings are == or .equals()
58       *
59       * @param s1 string 1
60       * @param s2 string 2
61       * @return true if equal
62       */
63      private static boolean equalStrings(String s1, String s2) {
64          if (s1 == s2) {
65              return true;
66          }
67          s1 = s1 == null ? "" : s1.trim();
68          s2 = s2 == null ? "" : s2.trim();
69          return s1.equals(s2);
70      }
71  
72      /**
73       * Find the nearest occurrence of the specified attribute on the specified and enclosing elements.
74       *
75       * @param n        current node
76       * @param attrName attribute name
77       * @param ns       Namespace
78       * @return attribute value
79       */
80      protected static String findEnclosingAttribute(Object n, final String attrName, final Namespace ns) {
81          while (n != null) {
82              if (n instanceof Element) {
83                  final Element e = (Element) n;
84                  final String attr = e.getAttributeValue(attrName, ns);
85                  if (attr != null && !attr.isEmpty()) {
86                      return attr;
87                  }
88              }
89              n = nodeParent(n);
90          }
91          return null;
92      }
93  
94      /**
95       * Gets the local name of the specified node.
96       *
97       * @param node to check
98       * @return String local name
99       */
100     public static String getLocalName(final Object node) {
101         if (node instanceof Element) {
102             return ((Element) node).getName();
103         }
104         if (node instanceof Attribute) {
105             return ((Attribute) node).getName();
106         }
107         return null;
108     }
109 
110     /**
111      * Gets the ns uri of the specified node.
112      *
113      * @param node Node to check
114      * @return String
115      */
116     private static String getNamespaceURI(final Object node) {
117         if (node instanceof Element) {
118             final Element element = (Element) node;
119             String ns = element.getNamespaceURI();
120             if ("".equals(ns)) {
121                 ns = null;
122             }
123             return ns;
124         }
125         return null;
126     }
127 
128     /**
129      * Gets the prefix from a given node.
130      *
131      * @param node to check
132      * @return String
133      */
134     public static String getPrefix(final Object node) {
135         if (node instanceof Element) {
136             final String prefix = ((Element) node).getNamespacePrefix();
137             return prefix == null || prefix.isEmpty() ? null : prefix;
138         }
139         if (node instanceof Attribute) {
140             final String prefix = ((Attribute) node).getNamespacePrefix();
141             return prefix == null || prefix.isEmpty() ? null : prefix;
142         }
143         return null;
144     }
145 
146     /**
147      * Gets the parent of the specified node.
148      *
149      * @param node to check
150      * @return parent Element
151      */
152     private static Element nodeParent(final Object node) {
153         if (node instanceof Element) {
154             final Object parent = ((Element) node).getParent();
155             return parent instanceof Element ? (Element) parent : null;
156         }
157         if (node instanceof Text) {
158             return (Element) ((Text) node).getParent();
159         }
160         if (node instanceof CDATA) {
161             return (Element) ((CDATA) node).getParent();
162         }
163         if (node instanceof ProcessingInstruction) {
164             return (Element) ((ProcessingInstruction) node).getParent();
165         }
166         if (node instanceof Comment) {
167             return (Element) ((Comment) node).getParent();
168         }
169         return null;
170     }
171 
172     /**
173      * Execute test against node on behalf of pointer.
174      *
175      * @param pointer Pointer
176      * @param node    to test
177      * @param test    to execute
178      * @return true if node passes test
179      */
180     public static boolean testNode(final NodePointer pointer, final Object node, final NodeTest test) {
181         if (test == null) {
182             return true;
183         }
184         if (test instanceof NodeNameTest) {
185             if (!(node instanceof Element)) {
186                 return false;
187             }
188             final NodeNameTest nodeNameTest = (NodeNameTest) test;
189             final QName testName = nodeNameTest.getNodeName();
190             final String namespaceURI = nodeNameTest.getNamespaceURI();
191             final boolean wildcard = nodeNameTest.isWildcard();
192             final String testPrefix = testName.getPrefix();
193             if (wildcard && testPrefix == null) {
194                 return true;
195             }
196             if (wildcard || testName.getName().equals(getLocalName(node))) {
197                 final String nodeNS = getNamespaceURI(node);
198                 return equalStrings(namespaceURI, nodeNS) || nodeNS == null && equalStrings(testPrefix, getPrefix(node));
199             }
200             return false;
201         }
202         if (test instanceof NodeTypeTest) {
203             switch (((NodeTypeTest) test).getNodeType()) {
204             case Compiler.NODE_TYPE_NODE:
205                 return true;
206             case Compiler.NODE_TYPE_TEXT:
207                 return node instanceof Text || node instanceof CDATA;
208             case Compiler.NODE_TYPE_COMMENT:
209                 return node instanceof Comment;
210             case Compiler.NODE_TYPE_PI:
211                 return node instanceof ProcessingInstruction;
212             default:
213                 return false;
214             }
215         }
216         if (test instanceof ProcessingInstructionTest && node instanceof ProcessingInstruction) {
217             final String testPI = ((ProcessingInstructionTest) test).getTarget();
218             final String nodePI = ((ProcessingInstruction) node).getTarget();
219             return testPI.equals(nodePI);
220         }
221         return false;
222     }
223 
224     /** Node, like a bean. */
225     private final Object node;
226 
227     /** Optional ID, may be null. */
228     private final String id;
229 
230     /** Local namespace resolver. */
231     private NamespaceResolver localNamespaceResolver;
232 
233     /**
234      * Constructs a new JDOMNodePointer.
235      *
236      * @param parent NodePointer
237      * @param node   pointed
238      */
239     public JDOMNodePointer(final NodePointer parent, final Object node) {
240         super(parent);
241         this.node = node;
242         this.id = null;
243     }
244 
245     /**
246      * Constructs a new JDOMNodePointer.
247      *
248      * @param node   node, like a bean.
249      * @param locale Locale
250      */
251     public JDOMNodePointer(final Object node, final Locale locale) {
252         super(null, locale);
253         this.node = node;
254         this.id = null;
255     }
256 
257     /**
258      * Constructs a new JDOMNodePointer.
259      *
260      * @param node   pointed
261      * @param locale Locale
262      * @param id     String id
263      */
264     public JDOMNodePointer(final Object node, final Locale locale, final String id) {
265         super(null, locale);
266         this.node = node;
267         this.id = id;
268     }
269 
270     /**
271      * Add the specified content to this element.
272      *
273      * @param content List
274      */
275     private void addContent(final List content) {
276         final Element element = (Element) node;
277         final int count = content.size();
278         for (int i = 0; i < count; i++) {
279             Object child = content.get(i);
280             if (child instanceof Element) {
281                 child = ((Element) child).clone();
282                 element.addContent((Element) child);
283             } else if (child instanceof Text) {
284                 child = ((Text) child).clone();
285                 element.addContent((Text) child);
286             } else if (node instanceof CDATA) {
287                 child = ((CDATA) child).clone();
288                 element.addContent((CDATA) child);
289             } else if (node instanceof ProcessingInstruction) {
290                 child = ((ProcessingInstruction) child).clone();
291                 element.addContent((ProcessingInstruction) child);
292             } else if (node instanceof Comment) {
293                 child = ((Comment) child).clone();
294                 element.addContent((Comment) child);
295             }
296         }
297     }
298 
299     @Override
300     public String asPath() {
301         if (id != null) {
302             return "id('" + escape(id) + "')";
303         }
304         final StringBuilder buffer = new StringBuilder();
305         if (parent != null) {
306             buffer.append(parent.asPath());
307         }
308         if (node instanceof Element) {
309             // If the parent pointer is not a JDOMNodePointer, it is
310             // the parent's responsibility to produce the node test part
311             // of the path
312             if (parent instanceof JDOMNodePointer) {
313                 if (buffer.length() == 0 || buffer.charAt(buffer.length() - 1) != '/') {
314                     buffer.append('/');
315                 }
316                 final String nsURI = getNamespaceURI();
317                 final String ln = getLocalName(node);
318                 if (nsURI == null) {
319                     buffer.append(ln);
320                     buffer.append('[');
321                     buffer.append(getRelativePositionByQName()).append(']');
322                 } else {
323                     final String prefix = getNamespaceResolver().getPrefix(nsURI);
324                     if (prefix != null) {
325                         buffer.append(prefix);
326                         buffer.append(':');
327                         buffer.append(ln);
328                         buffer.append('[');
329                         buffer.append(getRelativePositionByQName());
330                     } else {
331                         buffer.append("node()");
332                         buffer.append('[');
333                         buffer.append(getRelativePositionOfElement());
334                     }
335                     buffer.append(']');
336                 }
337             }
338         } else if (node instanceof Text || node instanceof CDATA) {
339             buffer.append("/text()");
340             buffer.append('[').append(getRelativePositionOfTextNode()).append(']');
341         } else if (node instanceof ProcessingInstruction) {
342             buffer.append("/processing-instruction(\'").append(((ProcessingInstruction) node).getTarget()).append("')");
343             buffer.append('[').append(getRelativePositionOfPI()).append(']');
344         }
345         return buffer.toString();
346     }
347 
348     @Override
349     public NodeIterator attributeIterator(final QName qName) {
350         return new JDOMAttributeIterator(this, qName);
351     }
352 
353     @Override
354     public NodeIterator childIterator(final NodeTest test, final boolean reverse, final NodePointer startWith) {
355         return new JDOMNodeIterator(this, test, reverse, startWith);
356     }
357 
358     @Override
359     public int compareChildNodePointers(final NodePointer pointer1, final NodePointer pointer2) {
360         final Object node1 = pointer1.getBaseValue();
361         final Object node2 = pointer2.getBaseValue();
362         if (node1 == node2) {
363             return 0;
364         }
365         if (node1 instanceof Attribute && !(node2 instanceof Attribute)) {
366             return -1;
367         }
368         if (!(node1 instanceof Attribute) && node2 instanceof Attribute) {
369             return 1;
370         }
371         if (node1 instanceof Attribute && node2 instanceof Attribute) {
372             final List list = ((Element) getNode()).getAttributes();
373             final int length = list.size();
374             for (int i = 0; i < length; i++) {
375                 final Object n = list.get(i);
376                 if (n == node1) {
377                     return -1;
378                 }
379                 if (n == node2) {
380                     return 1;
381                 }
382             }
383             return 0; // Should not happen
384         }
385         if (!(node instanceof Element)) {
386             throw new IllegalStateException("JXPath internal error: compareChildNodes called for " + node);
387         }
388         final List children = ((Element) node).getContent();
389         final int length = children.size();
390         for (int i = 0; i < length; i++) {
391             final Object n = children.get(i);
392             if (n == node1) {
393                 return -1;
394             }
395             if (n == node2) {
396                 return 1;
397             }
398         }
399         return 0;
400     }
401 
402     @Override
403     public NodePointer createAttribute(final JXPathContext context, final QName qName) {
404         if (!(node instanceof Element)) {
405             return super.createAttribute(context, qName);
406         }
407         final Element element = (Element) node;
408         final String prefix = qName.getPrefix();
409         if (prefix != null) {
410             final String namespaceUri = getNamespaceResolver().getNamespaceURI(prefix);
411             if (namespaceUri == null) {
412                 throw new JXPathException("Unknown namespace prefix: " + prefix);
413             }
414             final Namespace ns = Namespace.getNamespace(prefix, namespaceUri);
415             final Attribute attr = element.getAttribute(qName.getName(), ns);
416             if (attr == null) {
417                 element.setAttribute(qName.getName(), "", ns);
418             }
419         } else {
420             final Attribute attr = element.getAttribute(qName.getName());
421             if (attr == null) {
422                 element.setAttribute(qName.getName(), "");
423             }
424         }
425         final NodeIterator it = attributeIterator(qName);
426         it.setPosition(1);
427         return it.getNodePointer();
428     }
429 
430     @Override
431     public NodePointer createChild(final JXPathContext context, final QName qName, int index) {
432         if (index == WHOLE_COLLECTION) {
433             index = 0;
434         }
435         final boolean success = getAbstractFactory(context).createObject(context, this, node, qName.toString(), index);
436         if (success) {
437             NodeTest nodeTest;
438             final String prefix = qName.getPrefix();
439             final String namespaceURI = prefix == null ? null : context.getNamespaceURI(prefix);
440             nodeTest = new NodeNameTest(qName, namespaceURI);
441             final NodeIterator it = childIterator(nodeTest, false, null);
442             if (it != null && it.setPosition(index + 1)) {
443                 return it.getNodePointer();
444             }
445         }
446         throw new JXPathAbstractFactoryException("Factory could not create a child node for path: " + asPath() + "/" + qName + "[" + (index + 1) + "]");
447     }
448 
449     @Override
450     public NodePointer createChild(final JXPathContext context, final QName qName, final int index, final Object value) {
451         final NodePointer ptr = createChild(context, qName, index);
452         ptr.setValue(value);
453         return ptr;
454     }
455 
456     @Override
457     public boolean equals(final Object object) {
458         if (object == this) {
459             return true;
460         }
461         if (!(object instanceof JDOMNodePointer)) {
462             return false;
463         }
464         final JDOMNodePointer other = (JDOMNodePointer) object;
465         return node == other.node;
466     }
467 
468     @Override
469     public Object getBaseValue() {
470         return node;
471     }
472 
473     @Override
474     public Object getImmediateNode() {
475         return node;
476     }
477 
478     /**
479      * Gets the language of this element.
480      *
481      * @return String language
482      */
483     protected String getLanguage() {
484         return findEnclosingAttribute(node, "lang", Namespace.XML_NAMESPACE);
485     }
486 
487     @Override
488     public int getLength() {
489         return 1;
490     }
491 
492     @Override
493     public QName getName() {
494         String ns = null;
495         String ln = null;
496         if (node instanceof Element) {
497             ns = ((Element) node).getNamespacePrefix();
498             if (ns != null && ns.isEmpty()) {
499                 ns = null;
500             }
501             ln = ((Element) node).getName();
502         } else if (node instanceof ProcessingInstruction) {
503             ln = ((ProcessingInstruction) node).getTarget();
504         }
505         return new QName(ns, ln);
506     }
507 
508     @Override
509     public synchronized NamespaceResolver getNamespaceResolver() {
510         if (localNamespaceResolver == null) {
511             localNamespaceResolver = new NamespaceResolver(super.getNamespaceResolver());
512             localNamespaceResolver.setNamespaceContextPointer(this);
513         }
514         return localNamespaceResolver;
515     }
516 
517     @Override
518     public String getNamespaceURI() {
519         return getNamespaceURI(node);
520     }
521 
522     @Override
523     public String getNamespaceURI(final String prefix) {
524         if (prefix.equals("xml")) {
525             return Namespace.XML_NAMESPACE.getURI();
526         }
527         Element element = null;
528         if (node instanceof Document) {
529             element = ((Document) node).getRootElement();
530         }
531         if (node instanceof Element) {
532             element = (Element) node;
533         }
534         if (element == null) {
535             return null;
536         }
537         final Namespace ns = element.getNamespace(prefix);
538         return ns == null ? null : ns.getURI();
539     }
540 
541     /**
542      * Gets relative position of this among like-named siblings.
543      *
544      * @return 1..n
545      */
546     private int getRelativePositionByQName() {
547         if (node instanceof Element) {
548             final Object parent = ((Element) node).getParent();
549             if (!(parent instanceof Element)) {
550                 return 1;
551             }
552             final List children = ((Element) parent).getContent();
553             int count = 0;
554             for (final Object child : children) {
555                 if (child instanceof Element && matchesQName((Element) child)) {
556                     count++;
557                 }
558                 if (child == node) {
559                     break;
560                 }
561             }
562             return count;
563         }
564         return 1;
565     }
566 
567     /**
568      * Gets relative position of this among all siblings.
569      *
570      * @return 1..n
571      */
572     private int getRelativePositionOfElement() {
573         final Object parent = ((Element) node).getParent();
574         if (parent == null) {
575             return 1;
576         }
577         List children;
578         if (parent instanceof Element) {
579             children = ((Element) parent).getContent();
580         } else {
581             children = ((Document) parent).getContent();
582         }
583         int count = 0;
584         for (final Object child : children) {
585             if (child instanceof Element) {
586                 count++;
587             }
588             if (child == node) {
589                 break;
590             }
591         }
592         return count;
593     }
594 
595     /**
596      * Gets the relative position of this among same-target processing instruction siblings.
597      *
598      * @return 1..n
599      */
600     private int getRelativePositionOfPI() {
601         final String target = ((ProcessingInstruction) node).getTarget();
602         final Element parent = (Element) ((ProcessingInstruction) node).getParent();
603         if (parent == null) {
604             return 1;
605         }
606         final List children = parent.getContent();
607         int count = 0;
608         for (final Object child : children) {
609             if (child instanceof ProcessingInstruction && (target == null || target.equals(((ProcessingInstruction) child).getTarget()))) {
610                 count++;
611             }
612             if (child == node) {
613                 break;
614             }
615         }
616         return count;
617     }
618 
619     /**
620      * Gets the relative position of this among sibling text nodes.
621      *
622      * @return 1..n
623      */
624     private int getRelativePositionOfTextNode() {
625         Element parent;
626         if (node instanceof Text) {
627             parent = (Element) ((Text) node).getParent();
628         } else {
629             parent = (Element) ((CDATA) node).getParent();
630         }
631         if (parent == null) {
632             return 1;
633         }
634         final List children = parent.getContent();
635         int count = 0;
636         for (final Object child : children) {
637             if (child instanceof Text || child instanceof CDATA) {
638                 count++;
639             }
640             if (child == node) {
641                 break;
642             }
643         }
644         return count;
645     }
646 
647     @Override
648     public Object getValue() {
649         if (node instanceof Element) {
650             final StringBuilder buf = new StringBuilder();
651             for (final NodeIterator children = childIterator(null, false, null); children.setPosition(children.getPosition() + 1);) {
652                 final NodePointer ptr = children.getNodePointer();
653                 if (ptr.getImmediateNode() instanceof Element || ptr.getImmediateNode() instanceof Text) {
654                     buf.append(ptr.getValue());
655                 }
656             }
657             return buf.toString();
658         }
659         if (node instanceof Comment) {
660             String text = ((Comment) node).getText();
661             if (text != null) {
662                 text = text.trim();
663             }
664             return text;
665         }
666         String result = null;
667         if (node instanceof Text) {
668             result = ((Text) node).getText();
669         }
670         if (node instanceof ProcessingInstruction) {
671             result = ((ProcessingInstruction) node).getData();
672         }
673         final boolean trim = !"preserve".equals(findEnclosingAttribute(node, "space", Namespace.XML_NAMESPACE));
674         return result != null && trim ? result.trim() : result;
675     }
676 
677     @Override
678     public int hashCode() {
679         return node.hashCode();
680     }
681 
682     @Override
683     public boolean isCollection() {
684         return false;
685     }
686 
687     /**
688      * 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
689      * {@code super.isLanguage(lang)}.
690      *
691      * @param lang to compare
692      * @return true if this element uses the specified language.
693      */
694     @Override
695     public boolean isLanguage(final String lang) {
696         final String current = getLanguage();
697         return current == null ? super.isLanguage(lang) : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH));
698     }
699 
700     @Override
701     public boolean isLeaf() {
702         if (node instanceof Element) {
703             return ((Element) node).getContent().isEmpty();
704         }
705         if (node instanceof Document) {
706             return ((Document) node).getContent().isEmpty();
707         }
708         return true;
709     }
710 
711     private boolean matchesQName(final Element element) {
712         if (getNamespaceURI() != null) {
713             final String ns = getNamespaceURI(element);
714             if (ns == null || !ns.equals(getNamespaceURI())) {
715                 return false;
716             }
717         }
718         return element.getName().equals(((Element) node).getName());
719     }
720 
721     @Override
722     public NodeIterator namespaceIterator() {
723         return new JDOMNamespaceIterator(this);
724     }
725 
726     @Override
727     public NodePointer namespacePointer(final String prefix) {
728         return new JDOMNamespacePointer(this, prefix);
729     }
730 
731     @Override
732     public void remove() {
733         final Element parent = nodeParent(node);
734         if (parent == null) {
735             throw new JXPathException("Cannot remove root JDOM node");
736         }
737         parent.getContent().remove(node);
738     }
739 
740     @Override
741     public void setValue(final Object value) {
742         if (node instanceof Text) {
743             final String string = (String) TypeUtils.convert(value, String.class);
744             if (string != null && !string.isEmpty()) {
745                 ((Text) node).setText(string);
746             } else {
747                 nodeParent(node).removeContent((Text) node);
748             }
749         } else {
750             final Element element = (Element) node;
751             element.getContent().clear();
752             if (value instanceof Element) {
753                 final Element valueElement = (Element) value;
754                 addContent(valueElement.getContent());
755             } else if (value instanceof Document) {
756                 final Document valueDocument = (Document) value;
757                 addContent(valueDocument.getContent());
758             } else if (value instanceof Text || value instanceof CDATA) {
759                 final String string = ((Text) value).getText();
760                 element.addContent(new Text(string));
761             } else if (value instanceof ProcessingInstruction) {
762                 final ProcessingInstruction pi = (ProcessingInstruction) ((ProcessingInstruction) value).clone();
763                 element.addContent(pi);
764             } else if (value instanceof Comment) {
765                 final Comment comment = (Comment) ((Comment) value).clone();
766                 element.addContent(comment);
767             } else {
768                 final String string = (String) TypeUtils.convert(value, String.class);
769                 if (string != null && !string.isEmpty()) {
770                     element.addContent(new Text(string));
771                 }
772             }
773         }
774     }
775 
776     @Override
777     public boolean testNode(final NodeTest test) {
778         return testNode(this, node, test);
779     }
780 }