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