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