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 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 
DocumentImpl(DOMImplementationImpl impl, String namespaceURI, String qualifiedName, DocumentType doctype, String inputEncoding)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 
isXMLIdentifierStart(char c)91     private static boolean isXMLIdentifierStart(char c) {
92         return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_');
93     }
94 
isXMLIdentifierPart(char c)95     private static boolean isXMLIdentifierPart(char c) {
96         return isXMLIdentifierStart(c) || (c >= '0' && c <= '9') || (c == '-') || (c == '.');
97     }
98 
isXMLIdentifier(String s)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      */
shallowCopy(short operation, Node node)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      */
cloneOrImportNode(short operation, Node node, boolean deep)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 
importNode(Node importedNode, boolean deep)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      */
adoptNode(Node node)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      */
changeDocumentToThis(NodeImpl node)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 
renameNode(Node node, String namespaceURI, String qualifiedName)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 
createAttribute(String name)322     public AttrImpl createAttribute(String name) {
323         return new AttrImpl(this, name);
324     }
325 
createAttributeNS(String namespaceURI, String qualifiedName)326     public AttrImpl createAttributeNS(String namespaceURI, String qualifiedName) {
327         return new AttrImpl(this, namespaceURI, qualifiedName);
328     }
329 
createCDATASection(String data)330     public CDATASectionImpl createCDATASection(String data) {
331         return new CDATASectionImpl(this, data);
332     }
333 
createComment(String data)334     public CommentImpl createComment(String data) {
335         return new CommentImpl(this, data);
336     }
337 
createDocumentFragment()338     public DocumentFragmentImpl createDocumentFragment() {
339         return new DocumentFragmentImpl(this);
340     }
341 
createElement(String tagName)342     public ElementImpl createElement(String tagName) {
343         return new ElementImpl(this, tagName);
344     }
345 
createElementNS(String namespaceURI, String qualifiedName)346     public ElementImpl createElementNS(String namespaceURI, String qualifiedName) {
347         return new ElementImpl(this, namespaceURI, qualifiedName);
348     }
349 
createEntityReference(String name)350     public EntityReferenceImpl createEntityReference(String name) {
351         return new EntityReferenceImpl(this, name);
352     }
353 
createProcessingInstruction(String target, String data)354     public ProcessingInstructionImpl createProcessingInstruction(String target, String data) {
355         return new ProcessingInstructionImpl(this, target, data);
356     }
357 
createTextNode(String data)358     public TextImpl createTextNode(String data) {
359         return new TextImpl(this, data);
360     }
361 
getDoctype()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 
getDocumentElement()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 
getElementById(String elementId)382     public Element getElementById(String elementId) {
383         ElementImpl root = (ElementImpl) getDocumentElement();
384 
385         return (root == null ? null : root.getElementById(elementId));
386     }
387 
getElementsByTagName(String name)388     public NodeList getElementsByTagName(String name) {
389         NodeListImpl result = new NodeListImpl();
390         getElementsByTagName(result, name);
391         return result;
392     }
393 
getElementsByTagNameNS(String namespaceURI, String localName)394     public NodeList getElementsByTagNameNS(String namespaceURI, String localName) {
395         NodeListImpl result = new NodeListImpl();
396         getElementsByTagNameNS(result, namespaceURI, localName);
397         return result;
398     }
399 
getImplementation()400     public DOMImplementation getImplementation() {
401         return domImplementation;
402     }
403 
404     @Override
getNodeName()405     public String getNodeName() {
406         return "#document";
407     }
408 
409     @Override
getNodeType()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      */
insertChildAt(Node toInsert, int index)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 
getTextContent()430     @Override public String getTextContent() {
431         return null;
432     }
433 
getInputEncoding()434     public String getInputEncoding() {
435         return inputEncoding;
436     }
437 
getXmlEncoding()438     public String getXmlEncoding() {
439         return null;
440     }
441 
getXmlStandalone()442     public boolean getXmlStandalone() {
443         return xmlStandalone;
444     }
445 
setXmlStandalone(boolean xmlStandalone)446     public void setXmlStandalone(boolean xmlStandalone) {
447         this.xmlStandalone = xmlStandalone;
448     }
449 
getXmlVersion()450     public String getXmlVersion() {
451         return xmlVersion;
452     }
453 
setXmlVersion(String xmlVersion)454     public void setXmlVersion(String xmlVersion) {
455         this.xmlVersion = xmlVersion;
456     }
457 
getStrictErrorChecking()458     public boolean getStrictErrorChecking() {
459         return strictErrorChecking;
460     }
461 
setStrictErrorChecking(boolean strictErrorChecking)462     public void setStrictErrorChecking(boolean strictErrorChecking) {
463         this.strictErrorChecking = strictErrorChecking;
464     }
465 
getDocumentURI()466     public String getDocumentURI() {
467         return documentUri;
468     }
469 
setDocumentURI(String documentUri)470     public void setDocumentURI(String documentUri) {
471         this.documentUri = documentUri;
472     }
473 
getDomConfig()474     public DOMConfiguration getDomConfig() {
475         if (domConfiguration == null) {
476             domConfiguration = new DOMConfigurationImpl();
477         }
478         return domConfiguration;
479     }
480 
normalizeDocument()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      */
getUserDataMap(NodeImpl node)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      */
getUserDataMapForRead(NodeImpl node)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      */
notifyUserDataHandlers( short operation, Node source, NodeImpl destination)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