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