/*
* 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.
*
* 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.
*
*
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> 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 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 getUserDataMap(NodeImpl node) {
if (nodeToUserData == null) {
nodeToUserData = new WeakHashMap>();
}
Map userDataMap = nodeToUserData.get(node);
if (userDataMap == null) {
userDataMap = new HashMap();
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 getUserDataMapForRead(NodeImpl node) {
if (nodeToUserData == null) {
return Collections.emptyMap();
}
Map userDataMap = nodeToUserData.get(node);
return userDataMap == null
? Collections.emptyMap()
: userDataMap;
}
/**
* Calls {@link UserDataHandler#handle} on each of the source node's
* value/handler pairs.
*
* If the source node comes from another DOM implementation, user data
* handlers will not 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 entry
: srcImpl.document.getUserDataMapForRead(srcImpl).entrySet()) {
UserData userData = entry.getValue();
if (userData.handler != null) {
userData.handler.handle(
operation, entry.getKey(), userData.value, source, destination);
}
}
}
}