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