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