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