DocumentImpl.java revision f33eae7e84eb6d3b0f4e86b59605bb3de73009f3
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.Comment;
22import org.w3c.dom.DOMConfiguration;
23import org.w3c.dom.DOMException;
24import org.w3c.dom.DOMImplementation;
25import org.w3c.dom.Document;
26import org.w3c.dom.DocumentType;
27import org.w3c.dom.Element;
28import org.w3c.dom.NamedNodeMap;
29import org.w3c.dom.Node;
30import org.w3c.dom.NodeList;
31import org.w3c.dom.ProcessingInstruction;
32import org.w3c.dom.Text;
33import org.w3c.dom.UserDataHandler;
34
35import java.util.Collections;
36import java.util.HashMap;
37import java.util.Map;
38import java.util.WeakHashMap;
39
40/**
41 * Provides a straightforward implementation of the corresponding W3C DOM
42 * interface. The class is used internally only, thus only notable members that
43 * are not in the original interface are documented (the W3C docs are quite
44 * extensive). Hope that's ok.
45 * <p>
46 * Some of the fields may have package visibility, so other classes belonging to
47 * the DOM implementation can easily access them while maintaining the DOM tree
48 * structure.
49 */
50public final class DocumentImpl extends InnerNodeImpl implements Document {
51
52    private DOMImplementation domImplementation;
53    private DOMConfigurationImpl domConfiguration;
54
55    /*
56     * The default values of these fields are specified by the Document
57     * interface.
58     */
59    private String documentUri;
60    private String inputEncoding;
61    private String xmlEncoding;
62    private String xmlVersion = "1.0";
63    private boolean xmlStandalone = false;
64    private boolean strictErrorChecking = true;
65
66    /**
67     * A lazily initialized map of user data values for this document's own
68     * nodes. The map is weak because the document may live longer than its
69     * nodes.
70     *
71     * <p>Attaching user data directly to the corresponding node would cost a
72     * field per node. Under the assumption that user data is rarely needed, we
73     * attach user data to the document to save those fields. Xerces also takes
74     * this approach.
75     */
76    private WeakHashMap<NodeImpl, Map<String, UserData>> nodeToUserData;
77
78    public DocumentImpl(DOMImplementationImpl impl, String namespaceURI,
79            String qualifiedName, DocumentType doctype, String inputEncoding) {
80        super(null);
81
82        this.domImplementation = impl;
83        this.inputEncoding = inputEncoding;
84        this.document = this;
85
86        if (doctype != null) {
87            appendChild(doctype);
88        }
89
90        if (qualifiedName != null) {
91            appendChild(createElementNS(namespaceURI, qualifiedName));
92        }
93    }
94
95    private static boolean isXMLIdentifierStart(char c) {
96        return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_');
97    }
98
99    private static boolean isXMLIdentifierPart(char c) {
100        return isXMLIdentifierStart(c) || (c >= '0' && c <= '9') || (c == '-') || (c == '.');
101    }
102
103    static boolean isXMLIdentifier(String s) {
104        if (s.length() == 0) {
105            return false;
106        }
107
108        if (!isXMLIdentifierStart(s.charAt(0))) {
109            return false;
110        }
111
112        for (int i = 1; i < s.length(); i++) {
113            if (!isXMLIdentifierPart(s.charAt(i))) {
114                return false;
115            }
116        }
117
118        return true;
119    }
120
121    /**
122     * Returns a shallow copy of the given node. If the node is an element node,
123     * its attributes are always copied.
124     *
125     * @param node a node belonging to any document or DOM implementation.
126     * @param operation the operation type to use when notifying user data
127     *     handlers of copied element attributes. It is the caller's
128     *     responsibility to notify user data handlers of the returned node.
129     * @return a new node whose document is this document and whose DOM
130     *     implementation is this DOM implementation.
131     */
132    private NodeImpl shallowCopy(short operation, Node node) {
133        switch (node.getNodeType()) {
134            case Node.ATTRIBUTE_NODE:
135                Attr attr = (Attr) node;
136                AttrImpl attrCopy = createAttributeNS(attr.getNamespaceURI(), attr.getLocalName());
137                attrCopy.setPrefix(attr.getPrefix());
138                attrCopy.setNodeValue(attr.getNodeValue());
139                return attrCopy;
140
141            case Node.CDATA_SECTION_NODE:
142                return createCDATASection(((CharacterData) node).getData());
143
144            case Node.COMMENT_NODE:
145                return createComment(((Comment) node).getData());
146
147            case Node.DOCUMENT_FRAGMENT_NODE:
148                return createDocumentFragment();
149
150            case Node.DOCUMENT_NODE:
151            case Node.DOCUMENT_TYPE_NODE:
152                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
153                        "Cannot copy node of type " + node.getNodeType());
154
155            case Node.ELEMENT_NODE:
156                Element element = (Element) node;
157                ElementImpl elementCopy = createElementNS(
158                        element.getNamespaceURI(), element.getLocalName());
159                elementCopy.setPrefix(element.getPrefix());
160                NamedNodeMap attributes = element.getAttributes();
161                for (int i = 0; i < attributes.getLength(); i++) {
162                    Node elementAttr = attributes.item(i);
163                    AttrImpl elementAttrCopy = (AttrImpl) shallowCopy(operation, elementAttr);
164                    notifyUserDataHandlers(operation, elementAttr, elementAttrCopy);
165                    elementCopy.setAttributeNodeNS(elementAttrCopy);
166                }
167                return elementCopy;
168
169            case Node.ENTITY_NODE:
170            case Node.NOTATION_NODE:
171                // TODO: implement this when we support these node types
172                throw new UnsupportedOperationException();
173
174            case Node.ENTITY_REFERENCE_NODE:
175                /*
176                 * When we support entities in the doctype, this will need to
177                 * behave differently for clones vs. imports. Clones copy
178                 * entities by value, copying the referenced subtree from the
179                 * original document. Imports copy entities by reference,
180                 * possibly referring to a different subtree in the new
181                 * document.
182                 */
183                return createEntityReference(node.getNodeName());
184
185            case Node.PROCESSING_INSTRUCTION_NODE:
186                ProcessingInstruction pi = (ProcessingInstruction) node;
187                return createProcessingInstruction(pi.getTarget(), pi.getData());
188
189            case Node.TEXT_NODE:
190                return createTextNode(((Text) node).getData());
191
192            default:
193                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
194                        "Unsupported node type " + node.getNodeType());
195        }
196    }
197
198    /**
199     * Returns a copy of the given node or subtree with this document as its
200     * owner.
201     *
202     * @param operation either {@link UserDataHandler#NODE_CLONED} or
203     *      {@link UserDataHandler#NODE_IMPORTED}.
204     * @param node a node belonging to any document or DOM implementation.
205     * @param deep true to recursively copy any child nodes; false to do no such
206     *      copying and return a node with no children.
207     */
208    Node cloneOrImportNode(short operation, Node node, boolean deep) {
209        NodeImpl copy = shallowCopy(operation, node);
210
211        if (deep) {
212            NodeList list = node.getChildNodes();
213            for (int i = 0; i < list.getLength(); i++) {
214                copy.appendChild(cloneOrImportNode(operation, list.item(i), deep));
215            }
216        }
217
218        notifyUserDataHandlers(operation, node, copy);
219        return copy;
220    }
221
222    public Node importNode(Node importedNode, boolean deep) {
223        return cloneOrImportNode(UserDataHandler.NODE_IMPORTED, importedNode, deep);
224    }
225
226    /**
227     * Detaches the node from its parent (if any) and changes its document to
228     * this document. The node's subtree and attributes will remain attached,
229     * but their document will be changed to this document.
230     */
231    public Node adoptNode(Node node) {
232        if (!(node instanceof NodeImpl)) {
233            return null; // the API specifies this quiet failure
234        }
235        NodeImpl nodeImpl = (NodeImpl) node;
236        switch (nodeImpl.getNodeType()) {
237            case Node.ATTRIBUTE_NODE:
238                AttrImpl attr = (AttrImpl) node;
239                if (attr.ownerElement != null) {
240                    attr.ownerElement.removeAttributeNode(attr);
241                }
242                break;
243
244            case Node.DOCUMENT_FRAGMENT_NODE:
245            case Node.ENTITY_REFERENCE_NODE:
246            case Node.PROCESSING_INSTRUCTION_NODE:
247            case Node.TEXT_NODE:
248            case Node.CDATA_SECTION_NODE:
249            case Node.COMMENT_NODE:
250            case Node.ELEMENT_NODE:
251                break;
252
253            case Node.DOCUMENT_NODE:
254            case Node.DOCUMENT_TYPE_NODE:
255            case Node.ENTITY_NODE:
256            case Node.NOTATION_NODE:
257                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
258                        "Cannot adopt nodes of type " + nodeImpl.getNodeType());
259
260            default:
261                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
262                        "Unsupported node type " + node.getNodeType());
263        }
264
265        Node parent = nodeImpl.getParentNode();
266        if (parent != null) {
267            parent.removeChild(nodeImpl);
268        }
269
270        changeDocumentToThis(nodeImpl);
271        notifyUserDataHandlers(UserDataHandler.NODE_ADOPTED, node, null);
272        return nodeImpl;
273    }
274
275    /**
276     * Recursively change the document of {@code node} without also changing its
277     * parent node. Only adoptNode() should invoke this method, otherwise nodes
278     * will be left in an inconsistent state.
279     */
280    private void changeDocumentToThis(NodeImpl node) {
281        Map<String, UserData> userData = node.document.getUserDataMapForRead(node);
282        if (!userData.isEmpty()) {
283            getUserDataMap(node).putAll(userData);
284        }
285        node.document = this;
286
287        // change the document on all child nodes
288        NodeList list = node.getChildNodes();
289        for (int i = 0; i < list.getLength(); i++) {
290            changeDocumentToThis((NodeImpl) list.item(i));
291        }
292
293        // change the document on all attribute nodes
294        if (node.getNodeType() == Node.ELEMENT_NODE) {
295            NamedNodeMap attributes = node.getAttributes();
296            for (int i = 0; i < attributes.getLength(); i++) {
297                changeDocumentToThis((AttrImpl) attributes.item(i));
298            }
299        }
300    }
301
302    public Node renameNode(Node node, String namespaceURI, String qualifiedName) {
303        if (node.getOwnerDocument() != this) {
304            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, null);
305        }
306
307        setNameNS((NodeImpl) node, namespaceURI, qualifiedName);
308        notifyUserDataHandlers(UserDataHandler.NODE_RENAMED, node, null);
309        return node;
310    }
311
312    public AttrImpl createAttribute(String name) {
313        return new AttrImpl(this, name);
314    }
315
316    public AttrImpl createAttributeNS(String namespaceURI, String qualifiedName) {
317        return new AttrImpl(this, namespaceURI, qualifiedName);
318    }
319
320    public CDATASectionImpl createCDATASection(String data) {
321        return new CDATASectionImpl(this, data);
322    }
323
324    public CommentImpl createComment(String data) {
325        return new CommentImpl(this, data);
326    }
327
328    public DocumentFragmentImpl createDocumentFragment() {
329        return new DocumentFragmentImpl(this);
330    }
331
332    public ElementImpl createElement(String tagName) {
333        return new ElementImpl(this, tagName);
334    }
335
336    public ElementImpl createElementNS(String namespaceURI, String qualifiedName) {
337        return new ElementImpl(this, namespaceURI, qualifiedName);
338    }
339
340    public EntityReferenceImpl createEntityReference(String name) {
341        return new EntityReferenceImpl(this, name);
342    }
343
344    public ProcessingInstructionImpl createProcessingInstruction(String target, String data) {
345        return new ProcessingInstructionImpl(this, target, data);
346    }
347
348    public TextImpl createTextNode(String data) {
349        return new TextImpl(this, data);
350    }
351
352    public DocumentType getDoctype() {
353        for (LeafNodeImpl child : children) {
354            if (child instanceof DocumentType) {
355                return (DocumentType) child;
356            }
357        }
358
359        return null;
360    }
361
362    public Element getDocumentElement() {
363        for (LeafNodeImpl child : children) {
364            if (child instanceof Element) {
365                return (Element) child;
366            }
367        }
368
369        return null;
370    }
371
372    public Element getElementById(String elementId) {
373        ElementImpl root = (ElementImpl) getDocumentElement();
374
375        return (root == null ? null : root.getElementById(elementId));
376    }
377
378    public NodeList getElementsByTagName(String tagname) {
379        ElementImpl root = (ElementImpl) getDocumentElement();
380
381        return (root == null ? new NodeListImpl()
382                : root.getElementsByTagName(tagname));
383    }
384
385    public NodeList getElementsByTagNameNS(String namespaceURI, String localName) {
386        ElementImpl root = (ElementImpl) getDocumentElement();
387
388        return (root == null ? new NodeListImpl() : root.getElementsByTagNameNS(
389                namespaceURI, localName));
390    }
391
392    public DOMImplementation getImplementation() {
393        return domImplementation;
394    }
395
396    @Override
397    public String getNodeName() {
398        return "#document";
399    }
400
401    @Override
402    public short getNodeType() {
403        return Node.DOCUMENT_NODE;
404    }
405
406    @Override
407    public Node insertChildAt(Node newChild, int index) {
408        // Make sure we have at most one root element and one DTD element.
409        if (newChild instanceof Element && getDocumentElement() != null) {
410            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
411                    "Only one root element allowed");
412        } else if (newChild instanceof DocumentType && getDoctype() != null) {
413            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
414                    "Only one DOCTYPE element allowed");
415        }
416
417        return super.insertChildAt(newChild, index);
418    }
419
420    @Override public String getTextContent() {
421        return null;
422    }
423
424    public String getInputEncoding() {
425        return inputEncoding;
426    }
427
428    public String getXmlEncoding() {
429        return xmlEncoding;
430    }
431
432    public boolean getXmlStandalone() {
433        return xmlStandalone;
434    }
435
436    public void setXmlStandalone(boolean xmlStandalone) {
437        this.xmlStandalone = xmlStandalone;
438    }
439
440    public String getXmlVersion() {
441        return xmlVersion;
442    }
443
444    public void setXmlVersion(String xmlVersion) {
445        this.xmlVersion = xmlVersion;
446    }
447
448    public boolean getStrictErrorChecking() {
449        return strictErrorChecking;
450    }
451
452    public void setStrictErrorChecking(boolean strictErrorChecking) {
453        this.strictErrorChecking = strictErrorChecking;
454    }
455
456    public String getDocumentURI() {
457        return documentUri;
458    }
459
460    public void setDocumentURI(String documentUri) {
461        this.documentUri = documentUri;
462    }
463
464    public DOMConfiguration getDomConfig() {
465        if (domConfiguration == null) {
466            domConfiguration = new DOMConfigurationImpl();
467        }
468        return domConfiguration;
469    }
470
471    public void normalizeDocument() {
472        Element root = getDocumentElement();
473        if (root == null) {
474            return;
475        }
476
477        ((DOMConfigurationImpl) getDomConfig()).normalize(root);
478    }
479
480    /**
481     * Returns a map with the user data objects attached to the specified node.
482     * This map is readable and writable.
483     */
484    Map<String, UserData> getUserDataMap(NodeImpl node) {
485        if (nodeToUserData == null) {
486            nodeToUserData = new WeakHashMap<NodeImpl, Map<String, UserData>>();
487        }
488        Map<String, UserData> userDataMap = nodeToUserData.get(node);
489        if (userDataMap == null) {
490            userDataMap = new HashMap<String, UserData>();
491            nodeToUserData.put(node, userDataMap);
492        }
493        return userDataMap;
494    }
495
496    /**
497     * Returns a map with the user data objects attached to the specified node.
498     * The returned map may be read-only.
499     */
500    Map<String, UserData> getUserDataMapForRead(NodeImpl node) {
501        if (nodeToUserData == null) {
502            return Collections.emptyMap();
503        }
504        Map<String, UserData> userDataMap = nodeToUserData.get(node);
505        return userDataMap == null
506                ? Collections.<String, UserData>emptyMap()
507                : userDataMap;
508    }
509
510    /**
511     * Calls {@link UserDataHandler#handle} on each of the source node's
512     * value/handler pairs.
513     *
514     * <p>If the source node comes from another DOM implementation, user data
515     * handlers will <strong>not</strong> be notified. The DOM API provides no
516     * mechanism to inspect a foreign node's user data.
517     */
518    private static void notifyUserDataHandlers(
519            short operation, Node source, NodeImpl destination) {
520        if (!(source instanceof NodeImpl)) {
521            return;
522        }
523
524        NodeImpl srcImpl = (NodeImpl) source;
525        if (srcImpl.document == null) {
526            return;
527        }
528
529        for (Map.Entry<String, UserData> entry
530                : srcImpl.document.getUserDataMapForRead(srcImpl).entrySet()) {
531            UserData userData = entry.getValue();
532            if (userData.handler != null) {
533                userData.handler.handle(
534                        operation, entry.getKey(), userData.value, source, destination);
535            }
536        }
537    }
538}
539