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 
17 package org.apache.harmony.xml.dom;
18 
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.WeakHashMap;
23 import org.w3c.dom.CharacterData;
24 import org.w3c.dom.Comment;
25 import org.w3c.dom.DOMConfiguration;
26 import org.w3c.dom.DOMException;
27 import org.w3c.dom.DOMImplementation;
28 import org.w3c.dom.Document;
29 import org.w3c.dom.DocumentType;
30 import org.w3c.dom.Element;
31 import org.w3c.dom.NamedNodeMap;
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
34 import org.w3c.dom.ProcessingInstruction;
35 import org.w3c.dom.Text;
36 import 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  */
48 public 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 
DocumentImpl(DOMImplementationImpl impl, String namespaceURI, String qualifiedName, DocumentType doctype, String inputEncoding)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 
isXMLIdentifierStart(char c)92     private static boolean isXMLIdentifierStart(char c) {
93         return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_');
94     }
95 
isXMLIdentifierPart(char c)96     private static boolean isXMLIdentifierPart(char c) {
97         return isXMLIdentifierStart(c) || (c >= '0' && c <= '9') || (c == '-') || (c == '.');
98     }
99 
isXMLIdentifier(String s)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      */
shallowCopy(short operation, Node node)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      */
cloneOrImportNode(short operation, Node node, boolean deep)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 
importNode(Node importedNode, boolean deep)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      */
adoptNode(Node node)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      */
changeDocumentToThis(NodeImpl node)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 
renameNode(Node node, String namespaceURI, String qualifiedName)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 
createAttribute(String name)323     public AttrImpl createAttribute(String name) {
324         return new AttrImpl(this, name);
325     }
326 
createAttributeNS(String namespaceURI, String qualifiedName)327     public AttrImpl createAttributeNS(String namespaceURI, String qualifiedName) {
328         return new AttrImpl(this, namespaceURI, qualifiedName);
329     }
330 
createCDATASection(String data)331     public CDATASectionImpl createCDATASection(String data) {
332         return new CDATASectionImpl(this, data);
333     }
334 
createComment(String data)335     public CommentImpl createComment(String data) {
336         return new CommentImpl(this, data);
337     }
338 
createDocumentFragment()339     public DocumentFragmentImpl createDocumentFragment() {
340         return new DocumentFragmentImpl(this);
341     }
342 
createElement(String tagName)343     public ElementImpl createElement(String tagName) {
344         return new ElementImpl(this, tagName);
345     }
346 
createElementNS(String namespaceURI, String qualifiedName)347     public ElementImpl createElementNS(String namespaceURI, String qualifiedName) {
348         return new ElementImpl(this, namespaceURI, qualifiedName);
349     }
350 
createEntityReference(String name)351     public EntityReferenceImpl createEntityReference(String name) {
352         return new EntityReferenceImpl(this, name);
353     }
354 
createProcessingInstruction(String target, String data)355     public ProcessingInstructionImpl createProcessingInstruction(String target, String data) {
356         return new ProcessingInstructionImpl(this, target, data);
357     }
358 
createTextNode(String data)359     public TextImpl createTextNode(String data) {
360         return new TextImpl(this, data);
361     }
362 
getDoctype()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 
getDocumentElement()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 
getElementById(String elementId)383     public Element getElementById(String elementId) {
384         ElementImpl root = (ElementImpl) getDocumentElement();
385 
386         return (root == null ? null : root.getElementById(elementId));
387     }
388 
getElementsByTagName(String name)389     public NodeList getElementsByTagName(String name) {
390         NodeListImpl result = new NodeListImpl();
391         getElementsByTagName(result, name);
392         return result;
393     }
394 
getElementsByTagNameNS(String namespaceURI, String localName)395     public NodeList getElementsByTagNameNS(String namespaceURI, String localName) {
396         NodeListImpl result = new NodeListImpl();
397         getElementsByTagNameNS(result, namespaceURI, localName);
398         return result;
399     }
400 
getImplementation()401     public DOMImplementation getImplementation() {
402         return domImplementation;
403     }
404 
405     @Override
getNodeName()406     public String getNodeName() {
407         return "#document";
408     }
409 
410     @Override
getNodeType()411     public short getNodeType() {
412         return Node.DOCUMENT_NODE;
413     }
414 
415     /**
416      * Document elements may have at most one root element and at most one DTD
417      * element.
418      */
insertChildAt(Node toInsert, int index)419     @Override public Node insertChildAt(Node toInsert, int index) {
420         if (toInsert instanceof Element && getDocumentElement() != null) {
421             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
422                     "Only one root element allowed");
423         }
424         if (toInsert instanceof DocumentType && getDoctype() != null) {
425             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
426                     "Only one DOCTYPE element allowed");
427         }
428         return super.insertChildAt(toInsert, index);
429     }
430 
getTextContent()431     @Override public String getTextContent() {
432         return null;
433     }
434 
getInputEncoding()435     public String getInputEncoding() {
436         return inputEncoding;
437     }
438 
getXmlEncoding()439     public String getXmlEncoding() {
440         return xmlEncoding;
441     }
442 
getXmlStandalone()443     public boolean getXmlStandalone() {
444         return xmlStandalone;
445     }
446 
setXmlStandalone(boolean xmlStandalone)447     public void setXmlStandalone(boolean xmlStandalone) {
448         this.xmlStandalone = xmlStandalone;
449     }
450 
getXmlVersion()451     public String getXmlVersion() {
452         return xmlVersion;
453     }
454 
setXmlVersion(String xmlVersion)455     public void setXmlVersion(String xmlVersion) {
456         this.xmlVersion = xmlVersion;
457     }
458 
getStrictErrorChecking()459     public boolean getStrictErrorChecking() {
460         return strictErrorChecking;
461     }
462 
setStrictErrorChecking(boolean strictErrorChecking)463     public void setStrictErrorChecking(boolean strictErrorChecking) {
464         this.strictErrorChecking = strictErrorChecking;
465     }
466 
getDocumentURI()467     public String getDocumentURI() {
468         return documentUri;
469     }
470 
setDocumentURI(String documentUri)471     public void setDocumentURI(String documentUri) {
472         this.documentUri = documentUri;
473     }
474 
getDomConfig()475     public DOMConfiguration getDomConfig() {
476         if (domConfiguration == null) {
477             domConfiguration = new DOMConfigurationImpl();
478         }
479         return domConfiguration;
480     }
481 
normalizeDocument()482     public void normalizeDocument() {
483         Element root = getDocumentElement();
484         if (root == null) {
485             return;
486         }
487 
488         ((DOMConfigurationImpl) getDomConfig()).normalize(root);
489     }
490 
491     /**
492      * Returns a map with the user data objects attached to the specified node.
493      * This map is readable and writable.
494      */
getUserDataMap(NodeImpl node)495     Map<String, UserData> getUserDataMap(NodeImpl node) {
496         if (nodeToUserData == null) {
497             nodeToUserData = new WeakHashMap<NodeImpl, Map<String, UserData>>();
498         }
499         Map<String, UserData> userDataMap = nodeToUserData.get(node);
500         if (userDataMap == null) {
501             userDataMap = new HashMap<String, UserData>();
502             nodeToUserData.put(node, userDataMap);
503         }
504         return userDataMap;
505     }
506 
507     /**
508      * Returns a map with the user data objects attached to the specified node.
509      * The returned map may be read-only.
510      */
getUserDataMapForRead(NodeImpl node)511     Map<String, UserData> getUserDataMapForRead(NodeImpl node) {
512         if (nodeToUserData == null) {
513             return Collections.emptyMap();
514         }
515         Map<String, UserData> userDataMap = nodeToUserData.get(node);
516         return userDataMap == null
517                 ? Collections.<String, UserData>emptyMap()
518                 : userDataMap;
519     }
520 
521     /**
522      * Calls {@link UserDataHandler#handle} on each of the source node's
523      * value/handler pairs.
524      *
525      * <p>If the source node comes from another DOM implementation, user data
526      * handlers will <strong>not</strong> be notified. The DOM API provides no
527      * mechanism to inspect a foreign node's user data.
528      */
notifyUserDataHandlers( short operation, Node source, NodeImpl destination)529     private static void notifyUserDataHandlers(
530             short operation, Node source, NodeImpl destination) {
531         if (!(source instanceof NodeImpl)) {
532             return;
533         }
534 
535         NodeImpl srcImpl = (NodeImpl) source;
536         if (srcImpl.document == null) {
537             return;
538         }
539 
540         for (Map.Entry<String, UserData> entry
541                 : srcImpl.document.getUserDataMapForRead(srcImpl).entrySet()) {
542             UserData userData = entry.getValue();
543             if (userData.handler != null) {
544                 userData.handler.handle(
545                         operation, entry.getKey(), userData.value, source, destination);
546             }
547         }
548     }
549 }
550