/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.harmony.xml.dom; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; import org.w3c.dom.CharacterData; import org.w3c.dom.Comment; import org.w3c.dom.DOMConfiguration; import org.w3c.dom.DOMException; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.DocumentType; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; import org.w3c.dom.UserDataHandler; /** * Provides a straightforward implementation of the corresponding W3C DOM * interface. The class is used internally only, thus only notable members that * are not in the original interface are documented (the W3C docs are quite * extensive). Hope that's ok. * <p> * Some of the fields may have package visibility, so other classes belonging to * the DOM implementation can easily access them while maintaining the DOM tree * structure. */ public final class DocumentImpl extends InnerNodeImpl implements Document { private DOMImplementation domImplementation; private DOMConfigurationImpl domConfiguration; /* * The default values of these fields are specified by the Document * interface. */ private String documentUri; private String inputEncoding; private String xmlEncoding; private String xmlVersion = "1.0"; private boolean xmlStandalone = false; private boolean strictErrorChecking = true; /** * A lazily initialized map of user data values for this document's own * nodes. The map is weak because the document may live longer than its * nodes. * * <p>Attaching user data directly to the corresponding node would cost a * field per node. Under the assumption that user data is rarely needed, we * attach user data to the document to save those fields. Xerces also takes * this approach. */ private WeakHashMap<NodeImpl, Map<String, UserData>> nodeToUserData; public DocumentImpl(DOMImplementationImpl impl, String namespaceURI, String qualifiedName, DocumentType doctype, String inputEncoding) { super(null); this.document = this; this.domImplementation = impl; this.inputEncoding = inputEncoding; if (doctype != null) { appendChild(doctype); } if (qualifiedName != null) { appendChild(createElementNS(namespaceURI, qualifiedName)); } } private static boolean isXMLIdentifierStart(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_'); } private static boolean isXMLIdentifierPart(char c) { return isXMLIdentifierStart(c) || (c >= '0' && c <= '9') || (c == '-') || (c == '.'); } static boolean isXMLIdentifier(String s) { if (s.length() == 0) { return false; } if (!isXMLIdentifierStart(s.charAt(0))) { return false; } for (int i = 1; i < s.length(); i++) { if (!isXMLIdentifierPart(s.charAt(i))) { return false; } } return true; } /** * Returns a shallow copy of the given node. If the node is an element node, * its attributes are always copied. * * @param node a node belonging to any document or DOM implementation. * @param operation the operation type to use when notifying user data * handlers of copied element attributes. It is the caller's * responsibility to notify user data handlers of the returned node. * @return a new node whose document is this document and whose DOM * implementation is this DOM implementation. */ private NodeImpl shallowCopy(short operation, Node node) { switch (node.getNodeType()) { case Node.ATTRIBUTE_NODE: AttrImpl attr = (AttrImpl) node; AttrImpl attrCopy; if (attr.namespaceAware) { attrCopy = createAttributeNS(attr.getNamespaceURI(), attr.getLocalName()); attrCopy.setPrefix(attr.getPrefix()); } else { attrCopy = createAttribute(attr.getName()); } attrCopy.setNodeValue(attr.getValue()); return attrCopy; case Node.CDATA_SECTION_NODE: return createCDATASection(((CharacterData) node).getData()); case Node.COMMENT_NODE: return createComment(((Comment) node).getData()); case Node.DOCUMENT_FRAGMENT_NODE: return createDocumentFragment(); case Node.DOCUMENT_NODE: case Node.DOCUMENT_TYPE_NODE: throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot copy node of type " + node.getNodeType()); case Node.ELEMENT_NODE: ElementImpl element = (ElementImpl) node; ElementImpl elementCopy; if (element.namespaceAware) { elementCopy = createElementNS(element.getNamespaceURI(), element.getLocalName()); elementCopy.setPrefix(element.getPrefix()); } else { elementCopy = createElement(element.getTagName()); } NamedNodeMap attributes = element.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { AttrImpl elementAttr = (AttrImpl) attributes.item(i); AttrImpl elementAttrCopy = (AttrImpl) shallowCopy(operation, elementAttr); notifyUserDataHandlers(operation, elementAttr, elementAttrCopy); if (elementAttr.namespaceAware) { elementCopy.setAttributeNodeNS(elementAttrCopy); } else { elementCopy.setAttributeNode(elementAttrCopy); } } return elementCopy; case Node.ENTITY_NODE: case Node.NOTATION_NODE: // TODO: implement this when we support these node types throw new UnsupportedOperationException(); case Node.ENTITY_REFERENCE_NODE: /* * When we support entities in the doctype, this will need to * behave differently for clones vs. imports. Clones copy * entities by value, copying the referenced subtree from the * original document. Imports copy entities by reference, * possibly referring to a different subtree in the new * document. */ return createEntityReference(node.getNodeName()); case Node.PROCESSING_INSTRUCTION_NODE: ProcessingInstruction pi = (ProcessingInstruction) node; return createProcessingInstruction(pi.getTarget(), pi.getData()); case Node.TEXT_NODE: return createTextNode(((Text) node).getData()); default: throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Unsupported node type " + node.getNodeType()); } } /** * Returns a copy of the given node or subtree with this document as its * owner. * * @param operation either {@link UserDataHandler#NODE_CLONED} or * {@link UserDataHandler#NODE_IMPORTED}. * @param node a node belonging to any document or DOM implementation. * @param deep true to recursively copy any child nodes; false to do no such * copying and return a node with no children. */ Node cloneOrImportNode(short operation, Node node, boolean deep) { NodeImpl copy = shallowCopy(operation, node); if (deep) { NodeList list = node.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { copy.appendChild(cloneOrImportNode(operation, list.item(i), deep)); } } notifyUserDataHandlers(operation, node, copy); return copy; } public Node importNode(Node importedNode, boolean deep) { return cloneOrImportNode(UserDataHandler.NODE_IMPORTED, importedNode, deep); } /** * Detaches the node from its parent (if any) and changes its document to * this document. The node's subtree and attributes will remain attached, * but their document will be changed to this document. */ public Node adoptNode(Node node) { if (!(node instanceof NodeImpl)) { return null; // the API specifies this quiet failure } NodeImpl nodeImpl = (NodeImpl) node; switch (nodeImpl.getNodeType()) { case Node.ATTRIBUTE_NODE: AttrImpl attr = (AttrImpl) node; if (attr.ownerElement != null) { attr.ownerElement.removeAttributeNode(attr); } break; case Node.DOCUMENT_FRAGMENT_NODE: case Node.ENTITY_REFERENCE_NODE: case Node.PROCESSING_INSTRUCTION_NODE: case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: case Node.COMMENT_NODE: case Node.ELEMENT_NODE: break; case Node.DOCUMENT_NODE: case Node.DOCUMENT_TYPE_NODE: case Node.ENTITY_NODE: case Node.NOTATION_NODE: throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot adopt nodes of type " + nodeImpl.getNodeType()); default: throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Unsupported node type " + node.getNodeType()); } Node parent = nodeImpl.getParentNode(); if (parent != null) { parent.removeChild(nodeImpl); } changeDocumentToThis(nodeImpl); notifyUserDataHandlers(UserDataHandler.NODE_ADOPTED, node, null); return nodeImpl; } /** * Recursively change the document of {@code node} without also changing its * parent node. Only adoptNode() should invoke this method, otherwise nodes * will be left in an inconsistent state. */ private void changeDocumentToThis(NodeImpl node) { Map<String, UserData> userData = node.document.getUserDataMapForRead(node); if (!userData.isEmpty()) { getUserDataMap(node).putAll(userData); } node.document = this; // change the document on all child nodes NodeList list = node.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { changeDocumentToThis((NodeImpl) list.item(i)); } // change the document on all attribute nodes if (node.getNodeType() == Node.ELEMENT_NODE) { NamedNodeMap attributes = node.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { changeDocumentToThis((AttrImpl) attributes.item(i)); } } } public Node renameNode(Node node, String namespaceURI, String qualifiedName) { if (node.getOwnerDocument() != this) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, null); } setNameNS((NodeImpl) node, namespaceURI, qualifiedName); notifyUserDataHandlers(UserDataHandler.NODE_RENAMED, node, null); return node; } public AttrImpl createAttribute(String name) { return new AttrImpl(this, name); } public AttrImpl createAttributeNS(String namespaceURI, String qualifiedName) { return new AttrImpl(this, namespaceURI, qualifiedName); } public CDATASectionImpl createCDATASection(String data) { return new CDATASectionImpl(this, data); } public CommentImpl createComment(String data) { return new CommentImpl(this, data); } public DocumentFragmentImpl createDocumentFragment() { return new DocumentFragmentImpl(this); } public ElementImpl createElement(String tagName) { return new ElementImpl(this, tagName); } public ElementImpl createElementNS(String namespaceURI, String qualifiedName) { return new ElementImpl(this, namespaceURI, qualifiedName); } public EntityReferenceImpl createEntityReference(String name) { return new EntityReferenceImpl(this, name); } public ProcessingInstructionImpl createProcessingInstruction(String target, String data) { return new ProcessingInstructionImpl(this, target, data); } public TextImpl createTextNode(String data) { return new TextImpl(this, data); } public DocumentType getDoctype() { for (LeafNodeImpl child : children) { if (child instanceof DocumentType) { return (DocumentType) child; } } return null; } public Element getDocumentElement() { for (LeafNodeImpl child : children) { if (child instanceof Element) { return (Element) child; } } return null; } public Element getElementById(String elementId) { ElementImpl root = (ElementImpl) getDocumentElement(); return (root == null ? null : root.getElementById(elementId)); } public NodeList getElementsByTagName(String name) { NodeListImpl result = new NodeListImpl(); getElementsByTagName(result, name); return result; } public NodeList getElementsByTagNameNS(String namespaceURI, String localName) { NodeListImpl result = new NodeListImpl(); getElementsByTagNameNS(result, namespaceURI, localName); return result; } public DOMImplementation getImplementation() { return domImplementation; } @Override public String getNodeName() { return "#document"; } @Override public short getNodeType() { return Node.DOCUMENT_NODE; } /** * Document elements may have at most one root element and at most one DTD * element. */ @Override public Node insertChildAt(Node toInsert, int index) { if (toInsert instanceof Element && getDocumentElement() != null) { throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Only one root element allowed"); } if (toInsert instanceof DocumentType && getDoctype() != null) { throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Only one DOCTYPE element allowed"); } return super.insertChildAt(toInsert, index); } @Override public String getTextContent() { return null; } public String getInputEncoding() { return inputEncoding; } public String getXmlEncoding() { return xmlEncoding; } public boolean getXmlStandalone() { return xmlStandalone; } public void setXmlStandalone(boolean xmlStandalone) { this.xmlStandalone = xmlStandalone; } public String getXmlVersion() { return xmlVersion; } public void setXmlVersion(String xmlVersion) { this.xmlVersion = xmlVersion; } public boolean getStrictErrorChecking() { return strictErrorChecking; } public void setStrictErrorChecking(boolean strictErrorChecking) { this.strictErrorChecking = strictErrorChecking; } public String getDocumentURI() { return documentUri; } public void setDocumentURI(String documentUri) { this.documentUri = documentUri; } public DOMConfiguration getDomConfig() { if (domConfiguration == null) { domConfiguration = new DOMConfigurationImpl(); } return domConfiguration; } public void normalizeDocument() { Element root = getDocumentElement(); if (root == null) { return; } ((DOMConfigurationImpl) getDomConfig()).normalize(root); } /** * Returns a map with the user data objects attached to the specified node. * This map is readable and writable. */ Map<String, UserData> getUserDataMap(NodeImpl node) { if (nodeToUserData == null) { nodeToUserData = new WeakHashMap<NodeImpl, Map<String, UserData>>(); } Map<String, UserData> userDataMap = nodeToUserData.get(node); if (userDataMap == null) { userDataMap = new HashMap<String, UserData>(); nodeToUserData.put(node, userDataMap); } return userDataMap; } /** * Returns a map with the user data objects attached to the specified node. * The returned map may be read-only. */ Map<String, UserData> getUserDataMapForRead(NodeImpl node) { if (nodeToUserData == null) { return Collections.emptyMap(); } Map<String, UserData> userDataMap = nodeToUserData.get(node); return userDataMap == null ? Collections.<String, UserData>emptyMap() : userDataMap; } /** * Calls {@link UserDataHandler#handle} on each of the source node's * value/handler pairs. * * <p>If the source node comes from another DOM implementation, user data * handlers will <strong>not</strong> be notified. The DOM API provides no * mechanism to inspect a foreign node's user data. */ private static void notifyUserDataHandlers( short operation, Node source, NodeImpl destination) { if (!(source instanceof NodeImpl)) { return; } NodeImpl srcImpl = (NodeImpl) source; if (srcImpl.document == null) { return; } for (Map.Entry<String, UserData> entry : srcImpl.document.getUserDataMapForRead(srcImpl).entrySet()) { UserData userData = entry.getValue(); if (userData.handler != null) { userData.handler.handle( operation, entry.getKey(), userData.value, source, destination); } } } }