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