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.jdom;
019
020import java.util.List;
021import java.util.Locale;
022
023import org.apache.commons.jxpath.JXPathAbstractFactoryException;
024import org.apache.commons.jxpath.JXPathContext;
025import org.apache.commons.jxpath.JXPathException;
026import org.apache.commons.jxpath.ri.Compiler;
027import org.apache.commons.jxpath.ri.NamespaceResolver;
028import org.apache.commons.jxpath.ri.QName;
029import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
030import org.apache.commons.jxpath.ri.compiler.NodeTest;
031import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
032import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest;
033import org.apache.commons.jxpath.ri.model.NodeIterator;
034import org.apache.commons.jxpath.ri.model.NodePointer;
035import org.apache.commons.jxpath.util.TypeUtils;
036import org.jdom.Attribute;
037import org.jdom.CDATA;
038import org.jdom.Comment;
039import org.jdom.Document;
040import org.jdom.Element;
041import org.jdom.Namespace;
042import org.jdom.ProcessingInstruction;
043import org.jdom.Text;
044
045/**
046 * A Pointer that points to a DOM node.
047 */
048public class JDOMNodePointer extends NodePointer {
049
050    private static final long serialVersionUID = -6346532297491082651L;
051    /** XML ns uri */
052    public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
053    /** XMLNS ns uri */
054    public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
055
056    /**
057     * Tests whether two strings are == or .equals()
058     *
059     * @param s1 string 1
060     * @param s2 string 2
061     * @return true if equal
062     */
063    private static boolean equalStrings(String s1, String s2) {
064        if (s1 == s2) {
065            return true;
066        }
067        s1 = s1 == null ? "" : s1.trim();
068        s2 = s2 == null ? "" : s2.trim();
069        return s1.equals(s2);
070    }
071
072    /**
073     * Find the nearest occurrence of the specified attribute on the specified and enclosing elements.
074     *
075     * @param n        current node
076     * @param attrName attribute name
077     * @param ns       Namespace
078     * @return attribute value
079     */
080    protected static String findEnclosingAttribute(Object n, final String attrName, final Namespace ns) {
081        while (n != null) {
082            if (n instanceof Element) {
083                final Element e = (Element) n;
084                final String attr = e.getAttributeValue(attrName, ns);
085                if (attr != null && !attr.isEmpty()) {
086                    return attr;
087                }
088            }
089            n = nodeParent(n);
090        }
091        return null;
092    }
093
094    /**
095     * Gets the local name of the specified node.
096     *
097     * @param node to check
098     * @return String local name
099     */
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}