NodeImpl.java revision 9c075bb3e84c6eaafd016cbc1bf69a6e989eedf3
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    /** The containing document. Non-null. */
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 {@code node} to be namespace-aware and assigns its namespace URI
220     * and qualified name.
221     *
222     * @param node an element or attribute 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     * Sets {@code node} to be not namespace-aware and assigns its name.
272     *
273     * @param node an element or attribute node.
274     */
275    static void setName(NodeImpl node, String name) {
276        int prefixSeparator = name.lastIndexOf(":");
277        if (prefixSeparator != -1) {
278            String prefix = name.substring(0, prefixSeparator);
279            String localName = name.substring(prefixSeparator + 1);
280            if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) {
281                throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
282            }
283        } else if (!DocumentImpl.isXMLIdentifier(name)) {
284            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
285        }
286
287        switch (node.getNodeType()) {
288        case ATTRIBUTE_NODE:
289            AttrImpl attr = (AttrImpl) node;
290            attr.namespaceAware = false;
291            attr.localName = name;
292            break;
293
294        case ELEMENT_NODE:
295            ElementImpl element = (ElementImpl) node;
296            element.namespaceAware = false;
297            element.localName = name;
298            break;
299
300        default:
301            throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
302                    "Cannot rename nodes of type " + node.getNodeType());
303        }
304    }
305
306    public final String getBaseURI() {
307        switch (getNodeType()) {
308            case DOCUMENT_NODE:
309                return sanitizeUri(((Document) this).getDocumentURI());
310
311            case ELEMENT_NODE:
312                Element element = (Element) this;
313                String uri = element.getAttributeNS(
314                        "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base"
315
316                // if this node has no base URI, return the parent's.
317                if (uri == null || uri.length() == 0) {
318                    return getParentBaseUri();
319                }
320
321                // if this node's URI is absolute, return that
322                if (SystemIDResolver.isAbsoluteURI(uri)) {
323                    return uri;
324                }
325
326                // this node has a relative URI. Try to resolve it against the
327                // parent, but if that doesn't work just give up and return null.
328                String parentUri = getParentBaseUri();
329                if (parentUri == null) {
330                    return null;
331                }
332                try {
333                    return SystemIDResolver.getAbsoluteURI(uri, parentUri);
334                } catch (TransformerException e) {
335                    return null; // the spec requires that we swallow exceptions
336                }
337
338            case PROCESSING_INSTRUCTION_NODE:
339                return getParentBaseUri();
340
341            case NOTATION_NODE:
342            case ENTITY_NODE:
343                // When we support these node types, the parser should
344                // initialize a base URI field on these nodes.
345                return null;
346
347            case ENTITY_REFERENCE_NODE:
348                // TODO: get this value from the parser, falling back to the
349                // referenced entity's baseURI if that doesn't exist
350                return null;
351
352            case DOCUMENT_TYPE_NODE:
353            case DOCUMENT_FRAGMENT_NODE:
354            case ATTRIBUTE_NODE:
355            case TEXT_NODE:
356            case CDATA_SECTION_NODE:
357            case COMMENT_NODE:
358                return null;
359
360            default:
361                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
362                        "Unsupported node type " + getNodeType());
363        }
364    }
365
366    private String getParentBaseUri() {
367        Node parentNode = getParentNode();
368        return parentNode != null ? parentNode.getBaseURI() : null;
369    }
370
371    /**
372     * Returns the sanitized input if it is a URI, or {@code null} otherwise.
373     */
374    private String sanitizeUri(String uri) {
375        if (uri == null || uri.length() == 0) {
376            return null;
377        }
378        try {
379            return new URI(uri).toString();
380        } catch (URI.MalformedURIException e) {
381            return null;
382        }
383    }
384
385    public short compareDocumentPosition(Node other)
386            throws DOMException {
387        throw new UnsupportedOperationException(); // TODO
388    }
389
390    public String getTextContent() throws DOMException {
391        return getNodeValue();
392    }
393
394    void getTextContent(StringBuilder buf) throws DOMException {
395        String content = getNodeValue();
396        if (content != null) {
397            buf.append(content);
398        }
399    }
400
401    public final void setTextContent(String textContent) throws DOMException {
402        switch (getNodeType()) {
403            case DOCUMENT_TYPE_NODE:
404            case DOCUMENT_NODE:
405                return; // do nothing!
406
407            case ELEMENT_NODE:
408            case ENTITY_NODE:
409            case ENTITY_REFERENCE_NODE:
410            case DOCUMENT_FRAGMENT_NODE:
411                // remove all existing children
412                Node child;
413                while ((child = getFirstChild()) != null) {
414                    removeChild(child);
415                }
416                // create a text node to hold the given content
417                if (textContent != null && textContent.length() != 0) {
418                    appendChild(document.createTextNode(textContent));
419                }
420                return;
421
422            case ATTRIBUTE_NODE:
423            case TEXT_NODE:
424            case CDATA_SECTION_NODE:
425            case PROCESSING_INSTRUCTION_NODE:
426            case COMMENT_NODE:
427            case NOTATION_NODE:
428                setNodeValue(textContent);
429                return;
430
431            default:
432                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
433                        "Unsupported node type " + getNodeType());
434        }
435    }
436
437    public boolean isSameNode(Node other) {
438        return this == other;
439    }
440
441    /**
442     * Returns the element whose namespace definitions apply to this node. Use
443     * this element when mapping prefixes to URIs and vice versa.
444     */
445    private NodeImpl getNamespacingElement() {
446        switch (this.getNodeType()) {
447            case ELEMENT_NODE:
448                return this;
449
450            case DOCUMENT_NODE:
451                return (NodeImpl) ((Document) this).getDocumentElement();
452
453            case ENTITY_NODE:
454            case NOTATION_NODE:
455            case DOCUMENT_FRAGMENT_NODE:
456            case DOCUMENT_TYPE_NODE:
457                return null;
458
459            case ATTRIBUTE_NODE:
460                return (NodeImpl) ((Attr) this).getOwnerElement();
461
462            case TEXT_NODE:
463            case CDATA_SECTION_NODE:
464            case ENTITY_REFERENCE_NODE:
465            case PROCESSING_INSTRUCTION_NODE:
466            case COMMENT_NODE:
467                return getContainingElement();
468
469            default:
470                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
471                        "Unsupported node type " + getNodeType());
472        }
473    }
474
475    /**
476     * Returns the nearest ancestor element that contains this node.
477     */
478    private NodeImpl getContainingElement() {
479        for (Node p = getParentNode(); p != null; p = p.getParentNode()) {
480            if (p.getNodeType() == ELEMENT_NODE) {
481                return (NodeImpl) p;
482            }
483        }
484        return null;
485    }
486
487    public final String lookupPrefix(String namespaceURI) {
488        if (namespaceURI == null) {
489            return null;
490        }
491
492        // the XML specs define some prefixes (like "xml" and "xmlns") but this
493        // API is explicitly defined to ignore those.
494
495        NodeImpl target = getNamespacingElement();
496        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
497            // check this element's namespace first
498            if (namespaceURI.equals(node.getNamespaceURI())
499                    && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) {
500                return node.getPrefix();
501            }
502
503            // search this element for an attribute of this form:
504            //   xmlns:foo="http://namespaceURI"
505            if (!node.hasAttributes()) {
506                continue;
507            }
508            NamedNodeMap attributes = node.getAttributes();
509            for (int i = 0, length = attributes.getLength(); i < length; i++) {
510                Node attr = attributes.item(i);
511                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())
512                        || !"xmlns".equals(attr.getPrefix())
513                        || !namespaceURI.equals(attr.getNodeValue())) {
514                    continue;
515                }
516                if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) {
517                    return attr.getLocalName();
518                }
519            }
520        }
521
522        return null;
523    }
524
525    /**
526     * Returns true if the given prefix is mapped to the given URI on this
527     * element. Since child elements can redefine prefixes, this check is
528     * necessary: {@code
529     * <foo xmlns:a="http://good">
530     *   <bar xmlns:a="http://evil">
531     *     <a:baz />
532     *   </bar>
533     * </foo>}
534     *
535     * @param prefix the prefix to find. Nullable.
536     * @param uri the URI to match. Non-null.
537     */
538    boolean isPrefixMappedToUri(String prefix, String uri) {
539        if (prefix == null) {
540            return false;
541        }
542
543        String actual = lookupNamespaceURI(prefix);
544        return uri.equals(actual);
545    }
546
547    public final boolean isDefaultNamespace(String namespaceURI) {
548        String actual = lookupNamespaceURI(null); // null yields the default namespace
549        return namespaceURI == null
550                ? actual == null
551                : namespaceURI.equals(actual);
552    }
553
554    public final String lookupNamespaceURI(String prefix) {
555        NodeImpl target = getNamespacingElement();
556        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
557            // check this element's namespace first
558            String nodePrefix = node.getPrefix();
559            if (node.getNamespaceURI() != null) {
560                if (prefix == null // null => default prefix
561                        ? nodePrefix == null
562                        : prefix.equals(nodePrefix)) {
563                    return node.getNamespaceURI();
564                }
565            }
566
567            // search this element for an attribute of the appropriate form.
568            //    default namespace: xmlns="http://resultUri"
569            //          non default: xmlns:specifiedPrefix="http://resultUri"
570            if (!node.hasAttributes()) {
571                continue;
572            }
573            NamedNodeMap attributes = node.getAttributes();
574            for (int i = 0, length = attributes.getLength(); i < length; i++) {
575                Node attr = attributes.item(i);
576                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) {
577                    continue;
578                }
579                if (prefix == null // null => default prefix
580                        ? "xmlns".equals(attr.getNodeName())
581                        : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) {
582                    String value = attr.getNodeValue();
583                    return value.length() > 0 ? value : null;
584                }
585            }
586        }
587
588        return null;
589    }
590
591    /**
592     * Returns a list of objects such that two nodes are equal if their lists
593     * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes,
594     * neither of which override Object.equals(). Such values must be compared
595     * manually.
596     */
597    private static List<Object> createEqualityKey(Node node) {
598        List<Object> values = new ArrayList<Object>();
599        values.add(node.getNodeType());
600        values.add(node.getNodeName());
601        values.add(node.getLocalName());
602        values.add(node.getNamespaceURI());
603        values.add(node.getPrefix());
604        values.add(node.getNodeValue());
605        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
606            values.add(child);
607        }
608
609        switch (node.getNodeType()) {
610            case DOCUMENT_TYPE_NODE:
611                DocumentTypeImpl doctype = (DocumentTypeImpl) node;
612                values.add(doctype.getPublicId());
613                values.add(doctype.getSystemId());
614                values.add(doctype.getInternalSubset());
615                values.add(doctype.getEntities());
616                values.add(doctype.getNotations());
617                break;
618
619            case ELEMENT_NODE:
620                Element element = (Element) node;
621                values.add(element.getAttributes());
622                break;
623        }
624
625        return values;
626    }
627
628    public final boolean isEqualNode(Node arg) {
629        if (arg == this) {
630            return true;
631        }
632
633        List<Object> listA = createEqualityKey(this);
634        List<Object> listB = createEqualityKey(arg);
635
636        if (listA.size() != listB.size()) {
637            return false;
638        }
639
640        for (int i = 0; i < listA.size(); i++) {
641            Object a = listA.get(i);
642            Object b = listB.get(i);
643
644            if (a == b) {
645                continue;
646
647            } else if (a == null || b == null) {
648                return false;
649
650            } else if (a instanceof String || a instanceof Short) {
651                if (!a.equals(b)) {
652                    return false;
653                }
654
655            } else if (a instanceof NamedNodeMap) {
656                if (!(b instanceof NamedNodeMap)
657                        || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) {
658                    return false;
659                }
660
661            } else if (a instanceof Node) {
662                if (!(b instanceof Node)
663                        || !((Node) a).isEqualNode((Node) b)) {
664                    return false;
665                }
666
667            } else {
668                throw new AssertionError(); // unexpected type
669            }
670        }
671
672        return true;
673    }
674
675    private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) {
676        if (a.getLength() != b.getLength()) {
677            return false;
678        }
679        for (int i = 0; i < a.getLength(); i++) {
680            Node aNode = a.item(i);
681            Node bNode = aNode.getLocalName() == null
682                    ? b.getNamedItem(aNode.getNodeName())
683                    : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName());
684            if (bNode == null || !aNode.isEqualNode(bNode)) {
685                return false;
686            }
687        }
688        return true;
689    }
690
691    public final Object getFeature(String feature, String version) {
692        return isSupported(feature, version) ? this : null;
693    }
694
695    public final Object setUserData(String key, Object data, UserDataHandler handler) {
696        if (key == null) {
697            throw new NullPointerException();
698        }
699        Map<String, UserData> map = document.getUserDataMap(this);
700        UserData previous = data == null
701                ? map.remove(key)
702                : map.put(key, new UserData(data, handler));
703        return previous != null ? previous.value : null;
704    }
705
706    public final Object getUserData(String key) {
707        if (key == null) {
708            throw new NullPointerException();
709        }
710        Map<String, UserData> map = document.getUserDataMapForRead(this);
711        UserData userData = map.get(key);
712        return userData != null ? userData.value : null;
713    }
714
715    static class UserData {
716        final Object value;
717        final UserDataHandler handler;
718        UserData(Object value, UserDataHandler handler) {
719            this.value = value;
720            this.handler = handler;
721        }
722    }
723}
724