NodeImpl.java revision f33eae7e84eb6d3b0f4e86b59605bb3de73009f3
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.apache.harmony.xml.dom;
18
19import org.apache.xml.serializer.utils.SystemIDResolver;
20import org.apache.xml.utils.URI;
21import org.w3c.dom.Attr;
22import org.w3c.dom.CharacterData;
23import org.w3c.dom.DOMException;
24import org.w3c.dom.Document;
25import org.w3c.dom.Element;
26import org.w3c.dom.NamedNodeMap;
27import org.w3c.dom.Node;
28import org.w3c.dom.NodeList;
29import org.w3c.dom.ProcessingInstruction;
30import org.w3c.dom.TypeInfo;
31import org.w3c.dom.UserDataHandler;
32
33import javax.xml.transform.TransformerException;
34import java.util.ArrayList;
35import java.util.List;
36import java.util.Map;
37
38/**
39 * A straightforward implementation of the corresponding W3C DOM node.
40 *
41 * <p>Some fields have package visibility so other classes can access them while
42 * maintaining the DOM structure.
43 *
44 * <p>This class represents a Node that has neither a parent nor children.
45 * Subclasses may have either.
46 *
47 * <p>Some code was adapted from Apache Xerces.
48 */
49public abstract class NodeImpl implements Node {
50
51    private static final NodeList EMPTY_LIST = new NodeListImpl();
52
53    static final TypeInfo NULL_TYPE_INFO = new TypeInfo() {
54        public String getTypeName() {
55            return null;
56        }
57        public String getTypeNamespace() {
58            return null;
59        }
60        public boolean isDerivedFrom(
61                String typeNamespaceArg, String typeNameArg, int derivationMethod) {
62            return false;
63        }
64    };
65
66    DocumentImpl document;
67
68    NodeImpl(DocumentImpl document) {
69        this.document = document;
70    }
71
72    public Node appendChild(Node newChild) throws DOMException {
73        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
74    }
75
76    public final Node cloneNode(boolean deep) {
77        return document.cloneOrImportNode(UserDataHandler.NODE_CLONED, this, deep);
78    }
79
80    public NamedNodeMap getAttributes() {
81        return null;
82    }
83
84    public NodeList getChildNodes() {
85        return EMPTY_LIST;
86    }
87
88    public Node getFirstChild() {
89        return null;
90    }
91
92    public Node getLastChild() {
93        return null;
94    }
95
96    public String getLocalName() {
97        return null;
98    }
99
100    public String getNamespaceURI() {
101        return null;
102    }
103
104    public Node getNextSibling() {
105        return null;
106    }
107
108    public String getNodeName() {
109        return null;
110    }
111
112    public abstract short getNodeType();
113
114    public String getNodeValue() throws DOMException {
115        return null;
116    }
117
118    public final Document getOwnerDocument() {
119        return document == this ? null : document;
120    }
121
122    public Node getParentNode() {
123        return null;
124    }
125
126    public String getPrefix() {
127        return null;
128    }
129
130    public Node getPreviousSibling() {
131        return null;
132    }
133
134    public boolean hasAttributes() {
135        return false;
136    }
137
138    public boolean hasChildNodes() {
139        return false;
140    }
141
142    public Node insertBefore(Node newChild, Node refChild) throws DOMException {
143        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
144    }
145
146    public boolean isSupported(String feature, String version) {
147        return DOMImplementationImpl.getInstance().hasFeature(feature, version);
148    }
149
150    public void normalize() {
151    }
152
153    public Node removeChild(Node oldChild) throws DOMException {
154        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
155    }
156
157    public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
158        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
159    }
160
161    public final void setNodeValue(String nodeValue) throws DOMException {
162        switch (getNodeType()) {
163            case CDATA_SECTION_NODE:
164            case COMMENT_NODE:
165            case TEXT_NODE:
166                ((CharacterData) this).setData(nodeValue);
167                return;
168
169            case PROCESSING_INSTRUCTION_NODE:
170                ((ProcessingInstruction) this).setData(nodeValue);
171                return;
172
173            case ATTRIBUTE_NODE:
174                ((Attr) this).setValue(nodeValue);
175                return;
176
177            case ELEMENT_NODE:
178            case ENTITY_REFERENCE_NODE:
179            case ENTITY_NODE:
180            case DOCUMENT_NODE:
181            case DOCUMENT_TYPE_NODE:
182            case DOCUMENT_FRAGMENT_NODE:
183            case NOTATION_NODE:
184                return; // do nothing!
185
186            default:
187                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
188                        "Unsupported node type " + getNodeType());
189        }
190    }
191
192    public void setPrefix(String prefix) throws DOMException {
193    }
194
195    /**
196     * Validates the element or attribute namespace prefix on this node.
197     *
198     * @param namespaceAware whether this node is namespace aware
199     * @param namespaceURI this node's namespace URI
200     */
201    static String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) {
202        if (!namespaceAware) {
203            throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
204        }
205
206        if (prefix != null) {
207            if (namespaceURI == null
208                    || !DocumentImpl.isXMLIdentifier(prefix)
209                    || "xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI)
210                    || "xmlns".equals(prefix) && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
211                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
212            }
213        }
214
215        return prefix;
216    }
217
218    /**
219     * Sets the element or attribute node to be namespace-aware and assign it
220     * the specified name and namespace URI.
221     *
222     * @param node an AttrImpl or ElementImpl node.
223     * @param namespaceURI this node's namespace URI. May be null.
224     * @param qualifiedName a possibly-prefixed name like "img" or "html:img".
225     */
226    static void setNameNS(NodeImpl node, String namespaceURI, String qualifiedName) {
227        if (qualifiedName == null) {
228            throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
229        }
230
231        String prefix = null;
232        int p = qualifiedName.lastIndexOf(":");
233        if (p != -1) {
234            prefix = validatePrefix(qualifiedName.substring(0, p), true, namespaceURI);
235            qualifiedName = qualifiedName.substring(p + 1);
236        }
237
238        if (!DocumentImpl.isXMLIdentifier(qualifiedName)) {
239            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName);
240        }
241
242        switch (node.getNodeType()) {
243            case ATTRIBUTE_NODE:
244                if ("xmlns".equals(qualifiedName)
245                        && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
246                    throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
247                }
248
249                AttrImpl attr = (AttrImpl) node;
250                attr.namespaceAware = true;
251                attr.namespaceURI = namespaceURI;
252                attr.prefix = prefix;
253                attr.localName = qualifiedName;
254                break;
255
256            case ELEMENT_NODE:
257                ElementImpl element = (ElementImpl) node;
258                element.namespaceAware = true;
259                element.namespaceURI = namespaceURI;
260                element.prefix = prefix;
261                element.localName = qualifiedName;
262                break;
263
264            default:
265                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
266                        "Cannot rename nodes of type " + node.getNodeType());
267        }
268    }
269
270    /**
271     * Checks whether a required string matches an actual string. This utility
272     * method is used for comparing namespaces and such. It takes into account
273     * null arguments and the "*" special case.
274     *
275     * @param required The required string.
276     * @param actual The actual string.
277     * @return True if and only if the actual string matches the required one.
278     */
279    private static boolean matchesName(String required, String actual, boolean wildcard) {
280        if (wildcard && "*".equals(required)) {
281            return true;
282        }
283
284        if (required == null) {
285            return (actual == null);
286        }
287
288        return required.equals(actual);
289    }
290
291    /**
292     * Checks whether this node's name matches a required name. It takes into
293     * account null arguments and the "*" special case.
294     *
295     * @param name The required name.
296     * @return True if and only if the actual name matches the required one.
297     */
298    public boolean matchesName(String name, boolean wildcard) {
299        return matchesName(name, getNodeName(), wildcard);
300    }
301
302    /**
303     * Checks whether this node's namespace and local name match a required
304     * pair of namespace and local name. It takes into account null arguments
305     * and the "*" special case.
306     *
307     * @param namespaceURI The required namespace.
308     * @param localName The required local name.
309     * @return True if and only if the actual namespace and local name match
310     *         the required pair of namespace and local name.
311     */
312    public boolean matchesNameNS(String namespaceURI, String localName, boolean wildcard) {
313        return matchesName(namespaceURI, getNamespaceURI(), wildcard) && matchesName(localName, getLocalName(), wildcard);
314    }
315
316    public final String getBaseURI() {
317        switch (getNodeType()) {
318            case DOCUMENT_NODE:
319                return sanitizeUri(((Document) this).getDocumentURI());
320
321            case ELEMENT_NODE:
322                Element element = (Element) this;
323                String uri = element.getAttributeNS(
324                        "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base"
325
326                // if this node has no base URI, return the parent's.
327                if (uri == null || uri.length() == 0) {
328                    return getParentBaseUri();
329                }
330
331                // if this node's URI is absolute, return that
332                if (SystemIDResolver.isAbsoluteURI(uri)) {
333                    return uri;
334                }
335
336                // this node has a relative URI. Try to resolve it against the
337                // parent, but if that doesn't work just give up and return null.
338                String parentUri = getParentBaseUri();
339                if (parentUri == null) {
340                    return null;
341                }
342                try {
343                    return SystemIDResolver.getAbsoluteURI(uri, parentUri);
344                } catch (TransformerException e) {
345                    return null; // the spec requires that we swallow exceptions
346                }
347
348            case PROCESSING_INSTRUCTION_NODE:
349                return getParentBaseUri();
350
351            case NOTATION_NODE:
352            case ENTITY_NODE:
353                // When we support these node types, the parser should
354                // initialize a base URI field on these nodes.
355                return null;
356
357            case ENTITY_REFERENCE_NODE:
358                // TODO: get this value from the parser, falling back to the
359                // referenced entity's baseURI if that doesn't exist
360                return null;
361
362            case DOCUMENT_TYPE_NODE:
363            case DOCUMENT_FRAGMENT_NODE:
364            case ATTRIBUTE_NODE:
365            case TEXT_NODE:
366            case CDATA_SECTION_NODE:
367            case COMMENT_NODE:
368                return null;
369
370            default:
371                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
372                        "Unsupported node type " + getNodeType());
373        }
374    }
375
376    private String getParentBaseUri() {
377        Node parentNode = getParentNode();
378        return parentNode != null ? parentNode.getBaseURI() : null;
379    }
380
381    /**
382     * Returns the sanitized input if it is a URI, or {@code null} otherwise.
383     */
384    private String sanitizeUri(String uri) {
385        if (uri == null || uri.length() == 0) {
386            return null;
387        }
388        try {
389            return new URI(uri).toString();
390        } catch (URI.MalformedURIException e) {
391            return null;
392        }
393    }
394
395    public short compareDocumentPosition(Node other)
396            throws DOMException {
397        throw new UnsupportedOperationException(); // TODO
398    }
399
400    public String getTextContent() throws DOMException {
401        return getNodeValue();
402    }
403
404    void getTextContent(StringBuilder buf) throws DOMException {
405        String content = getNodeValue();
406        if (content != null) {
407            buf.append(content);
408        }
409    }
410
411    public final void setTextContent(String textContent) throws DOMException {
412        switch (getNodeType()) {
413            case DOCUMENT_TYPE_NODE:
414            case DOCUMENT_NODE:
415                return; // do nothing!
416
417            case ELEMENT_NODE:
418            case ENTITY_NODE:
419            case ENTITY_REFERENCE_NODE:
420            case DOCUMENT_FRAGMENT_NODE:
421                // remove all existing children
422                Node child;
423                while ((child = getFirstChild()) != null) {
424                    removeChild(child);
425                }
426                // create a text node to hold the given content
427                if (textContent != null && textContent.length() != 0) {
428                    appendChild(document.createTextNode(textContent));
429                }
430                return;
431
432            case ATTRIBUTE_NODE:
433            case TEXT_NODE:
434            case CDATA_SECTION_NODE:
435            case PROCESSING_INSTRUCTION_NODE:
436            case COMMENT_NODE:
437            case NOTATION_NODE:
438                setNodeValue(textContent);
439                return;
440
441            default:
442                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
443                        "Unsupported node type " + getNodeType());
444        }
445    }
446
447    public boolean isSameNode(Node other) {
448        return this == other;
449    }
450
451    /**
452     * Returns the element whose namespace definitions apply to this node. Use
453     * this element when mapping prefixes to URIs and vice versa.
454     */
455    private NodeImpl getNamespacingElement() {
456        switch (this.getNodeType()) {
457            case ELEMENT_NODE:
458                return this;
459
460            case DOCUMENT_NODE:
461                return (NodeImpl) ((Document) this).getDocumentElement();
462
463            case ENTITY_NODE:
464            case NOTATION_NODE:
465            case DOCUMENT_FRAGMENT_NODE:
466            case DOCUMENT_TYPE_NODE:
467                return null;
468
469            case ATTRIBUTE_NODE:
470                return (NodeImpl) ((Attr) this).getOwnerElement();
471
472            case TEXT_NODE:
473            case CDATA_SECTION_NODE:
474            case ENTITY_REFERENCE_NODE:
475            case PROCESSING_INSTRUCTION_NODE:
476            case COMMENT_NODE:
477                return getContainingElement();
478
479            default:
480                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
481                        "Unsupported node type " + getNodeType());
482        }
483    }
484
485    /**
486     * Returns the nearest ancestor element that contains this node.
487     */
488    private NodeImpl getContainingElement() {
489        for (Node p = getParentNode(); p != null; p = p.getParentNode()) {
490            if (p.getNodeType() == ELEMENT_NODE) {
491                return (NodeImpl) p;
492            }
493        }
494        return null;
495    }
496
497    public final String lookupPrefix(String namespaceURI) {
498        if (namespaceURI == null) {
499            return null;
500        }
501
502        // the XML specs define some prefixes (like "xml" and "xmlns") but this
503        // API is explicitly defined to ignore those.
504
505        NodeImpl target = getNamespacingElement();
506        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
507            // check this element's namespace first
508            if (namespaceURI.equals(node.getNamespaceURI())
509                    && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) {
510                return node.getPrefix();
511            }
512
513            // search this element for an attribute of this form:
514            //   xmlns:foo="http://namespaceURI"
515            if (!node.hasAttributes()) {
516                continue;
517            }
518            NamedNodeMap attributes = node.getAttributes();
519            for (int i = 0, length = attributes.getLength(); i < length; i++) {
520                Node attr = attributes.item(i);
521                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())
522                        || !"xmlns".equals(attr.getPrefix())
523                        || !namespaceURI.equals(attr.getNodeValue())) {
524                    continue;
525                }
526                if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) {
527                    return attr.getLocalName();
528                }
529            }
530        }
531
532        return null;
533    }
534
535    /**
536     * Returns true if the given prefix is mapped to the given URI on this
537     * element. Since child elements can redefine prefixes, this check is
538     * necessary: {@code
539     * <foo xmlns:a="http://good">
540     *   <bar xmlns:a="http://evil">
541     *     <a:baz />
542     *   </bar>
543     * </foo>}
544     *
545     * @param prefix the prefix to find. Nullable.
546     * @param uri the URI to match. Non-null.
547     */
548    boolean isPrefixMappedToUri(String prefix, String uri) {
549        if (prefix == null) {
550            return false;
551        }
552
553        String actual = lookupNamespaceURI(prefix);
554        return uri.equals(actual);
555    }
556
557    public final boolean isDefaultNamespace(String namespaceURI) {
558        String actual = lookupNamespaceURI(null); // null yields the default namespace
559        return namespaceURI == null
560                ? actual == null
561                : namespaceURI.equals(actual);
562    }
563
564    public final String lookupNamespaceURI(String prefix) {
565        NodeImpl target = getNamespacingElement();
566        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
567            // check this element's namespace first
568            String nodePrefix = node.getPrefix();
569            if (node.getNamespaceURI() != null) {
570                if (prefix == null // null => default prefix
571                        ? nodePrefix == null
572                        : prefix.equals(nodePrefix)) {
573                    return node.getNamespaceURI();
574                }
575            }
576
577            // search this element for an attribute of the appropriate form.
578            //    default namespace: xmlns="http://resultUri"
579            //          non default: xmlns:specifiedPrefix="http://resultUri"
580            if (!node.hasAttributes()) {
581                continue;
582            }
583            NamedNodeMap attributes = node.getAttributes();
584            for (int i = 0, length = attributes.getLength(); i < length; i++) {
585                Node attr = attributes.item(i);
586                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) {
587                    continue;
588                }
589                if (prefix == null // null => default prefix
590                        ? "xmlns".equals(attr.getNodeName())
591                        : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) {
592                    String value = attr.getNodeValue();
593                    return value.length() > 0 ? value : null;
594                }
595            }
596        }
597
598        return null;
599    }
600
601    /**
602     * Returns a list of objects such that two nodes are equal if their lists
603     * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes,
604     * neither of which override Object.equals(). Such values must be compared
605     * manually.
606     */
607    private static List<Object> createEqualityKey(Node node) {
608        List<Object> values = new ArrayList<Object>();
609        values.add(node.getNodeType());
610        values.add(node.getNodeName());
611        values.add(node.getLocalName());
612        values.add(node.getNamespaceURI());
613        values.add(node.getPrefix());
614        values.add(node.getNodeValue());
615        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
616            values.add(child);
617        }
618
619        switch (node.getNodeType()) {
620            case DOCUMENT_TYPE_NODE:
621                DocumentTypeImpl doctype = (DocumentTypeImpl) node;
622                values.add(doctype.getPublicId());
623                values.add(doctype.getSystemId());
624                values.add(doctype.getInternalSubset());
625                values.add(doctype.getEntities());
626                values.add(doctype.getNotations());
627                break;
628
629            case ELEMENT_NODE:
630                Element element = (Element) node;
631                values.add(element.getAttributes());
632                break;
633        }
634
635        return values;
636    }
637
638    public final boolean isEqualNode(Node arg) {
639        if (arg == this) {
640            return true;
641        }
642
643        List<Object> listA = createEqualityKey(this);
644        List<Object> listB = createEqualityKey(arg);
645
646        if (listA.size() != listB.size()) {
647            return false;
648        }
649
650        for (int i = 0; i < listA.size(); i++) {
651            Object a = listA.get(i);
652            Object b = listB.get(i);
653
654            if (a == b) {
655                continue;
656
657            } else if (a == null || b == null) {
658                return false;
659
660            } else if (a instanceof String || a instanceof Short) {
661                if (!a.equals(b)) {
662                    return false;
663                }
664
665            } else if (a instanceof NamedNodeMap) {
666                if (!(b instanceof NamedNodeMap)
667                        || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) {
668                    return false;
669                }
670
671            } else if (a instanceof Node) {
672                if (!(b instanceof Node)
673                        || !((Node) a).isEqualNode((Node) b)) {
674                    return false;
675                }
676
677            } else {
678                throw new AssertionError(); // unexpected type
679            }
680        }
681
682        return true;
683    }
684
685    private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) {
686        if (a.getLength() != b.getLength()) {
687            return false;
688        }
689        for (int i = 0; i < a.getLength(); i++) {
690            Node aNode = a.item(i);
691            Node bNode = aNode.getLocalName() == null
692                    ? b.getNamedItem(aNode.getNodeName())
693                    : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName());
694            if (bNode == null || !aNode.isEqualNode(bNode)) {
695                return false;
696            }
697        }
698        return true;
699    }
700
701    public final Object getFeature(String feature, String version) {
702        return isSupported(feature, version) ? this : null;
703    }
704
705    public final Object setUserData(String key, Object data, UserDataHandler handler) {
706        if (key == null) {
707            throw new NullPointerException();
708        }
709        Map<String, UserData> map = document.getUserDataMap(this);
710        UserData previous = data == null
711                ? map.remove(key)
712                : map.put(key, new UserData(data, handler));
713        return previous != null ? previous.value : null;
714    }
715
716    public final Object getUserData(String key) {
717        if (key == null) {
718            throw new NullPointerException();
719        }
720        Map<String, UserData> map = document.getUserDataMapForRead(this);
721        UserData userData = map.get(key);
722        return userData != null ? userData.value : null;
723    }
724
725    static class UserData {
726        final Object value;
727        final UserDataHandler handler;
728        UserData(Object value, UserDataHandler handler) {
729            this.value = value;
730            this.handler = handler;
731        }
732    }
733}
734