| /* |
| * 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.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.CharacterData; |
| import org.w3c.dom.DOMException; |
| import org.w3c.dom.Document; |
| 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.TypeInfo; |
| import org.w3c.dom.UserDataHandler; |
| |
| /** |
| * A straightforward implementation of the corresponding W3C DOM node. |
| * |
| * <p>Some fields have package visibility so other classes can access them while |
| * maintaining the DOM structure. |
| * |
| * <p>This class represents a Node that has neither a parent nor children. |
| * Subclasses may have either. |
| * |
| * <p>Some code was adapted from Apache Xerces. |
| */ |
| public abstract class NodeImpl implements Node { |
| |
| private static final NodeList EMPTY_LIST = new NodeListImpl(); |
| |
| static final TypeInfo NULL_TYPE_INFO = new TypeInfo() { |
| public String getTypeName() { |
| return null; |
| } |
| public String getTypeNamespace() { |
| return null; |
| } |
| public boolean isDerivedFrom( |
| String typeNamespaceArg, String typeNameArg, int derivationMethod) { |
| return false; |
| } |
| }; |
| |
| /** |
| * The containing document. This is non-null except for DocumentTypeImpl |
| * nodes created by the DOMImplementation. |
| */ |
| DocumentImpl document; |
| |
| NodeImpl(DocumentImpl document) { |
| this.document = document; |
| } |
| |
| public Node appendChild(Node newChild) throws DOMException { |
| throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); |
| } |
| |
| public final Node cloneNode(boolean deep) { |
| return document.cloneOrImportNode(UserDataHandler.NODE_CLONED, this, deep); |
| } |
| |
| public NamedNodeMap getAttributes() { |
| return null; |
| } |
| |
| public NodeList getChildNodes() { |
| return EMPTY_LIST; |
| } |
| |
| public Node getFirstChild() { |
| return null; |
| } |
| |
| public Node getLastChild() { |
| return null; |
| } |
| |
| public String getLocalName() { |
| return null; |
| } |
| |
| public String getNamespaceURI() { |
| return null; |
| } |
| |
| public Node getNextSibling() { |
| return null; |
| } |
| |
| public String getNodeName() { |
| return null; |
| } |
| |
| public abstract short getNodeType(); |
| |
| public String getNodeValue() throws DOMException { |
| return null; |
| } |
| |
| public final Document getOwnerDocument() { |
| return document == this ? null : document; |
| } |
| |
| public Node getParentNode() { |
| return null; |
| } |
| |
| public String getPrefix() { |
| return null; |
| } |
| |
| public Node getPreviousSibling() { |
| return null; |
| } |
| |
| public boolean hasAttributes() { |
| return false; |
| } |
| |
| public boolean hasChildNodes() { |
| return false; |
| } |
| |
| public Node insertBefore(Node newChild, Node refChild) throws DOMException { |
| throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); |
| } |
| |
| public boolean isSupported(String feature, String version) { |
| return DOMImplementationImpl.getInstance().hasFeature(feature, version); |
| } |
| |
| public void normalize() { |
| } |
| |
| public Node removeChild(Node oldChild) throws DOMException { |
| throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); |
| } |
| |
| public Node replaceChild(Node newChild, Node oldChild) throws DOMException { |
| throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); |
| } |
| |
| public final void setNodeValue(String nodeValue) throws DOMException { |
| switch (getNodeType()) { |
| case CDATA_SECTION_NODE: |
| case COMMENT_NODE: |
| case TEXT_NODE: |
| ((CharacterData) this).setData(nodeValue); |
| return; |
| |
| case PROCESSING_INSTRUCTION_NODE: |
| ((ProcessingInstruction) this).setData(nodeValue); |
| return; |
| |
| case ATTRIBUTE_NODE: |
| ((Attr) this).setValue(nodeValue); |
| return; |
| |
| case ELEMENT_NODE: |
| case ENTITY_REFERENCE_NODE: |
| case ENTITY_NODE: |
| case DOCUMENT_NODE: |
| case DOCUMENT_TYPE_NODE: |
| case DOCUMENT_FRAGMENT_NODE: |
| case NOTATION_NODE: |
| return; // do nothing! |
| |
| default: |
| throw new DOMException(DOMException.NOT_SUPPORTED_ERR, |
| "Unsupported node type " + getNodeType()); |
| } |
| } |
| |
| public void setPrefix(String prefix) throws DOMException { |
| } |
| |
| /** |
| * Validates the element or attribute namespace prefix on this node. |
| * |
| * @param namespaceAware whether this node is namespace aware |
| * @param namespaceURI this node's namespace URI |
| */ |
| static String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) { |
| if (!namespaceAware) { |
| throw new DOMException(DOMException.NAMESPACE_ERR, prefix); |
| } |
| |
| if (prefix != null) { |
| if (namespaceURI == null |
| || !DocumentImpl.isXMLIdentifier(prefix) |
| || "xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI) |
| || "xmlns".equals(prefix) && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) { |
| throw new DOMException(DOMException.NAMESPACE_ERR, prefix); |
| } |
| } |
| |
| return prefix; |
| } |
| |
| /** |
| * Sets {@code node} to be namespace-aware and assigns its namespace URI |
| * and qualified name. |
| * |
| * @param node an element or attribute node. |
| * @param namespaceURI this node's namespace URI. May be null. |
| * @param qualifiedName a possibly-prefixed name like "img" or "html:img". |
| */ |
| static void setNameNS(NodeImpl node, String namespaceURI, String qualifiedName) { |
| if (qualifiedName == null) { |
| throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName); |
| } |
| |
| String prefix = null; |
| int p = qualifiedName.lastIndexOf(":"); |
| if (p != -1) { |
| prefix = validatePrefix(qualifiedName.substring(0, p), true, namespaceURI); |
| qualifiedName = qualifiedName.substring(p + 1); |
| } |
| |
| if (!DocumentImpl.isXMLIdentifier(qualifiedName)) { |
| throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName); |
| } |
| |
| switch (node.getNodeType()) { |
| case ATTRIBUTE_NODE: |
| if ("xmlns".equals(qualifiedName) |
| && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) { |
| throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName); |
| } |
| |
| AttrImpl attr = (AttrImpl) node; |
| attr.namespaceAware = true; |
| attr.namespaceURI = namespaceURI; |
| attr.prefix = prefix; |
| attr.localName = qualifiedName; |
| break; |
| |
| case ELEMENT_NODE: |
| ElementImpl element = (ElementImpl) node; |
| element.namespaceAware = true; |
| element.namespaceURI = namespaceURI; |
| element.prefix = prefix; |
| element.localName = qualifiedName; |
| break; |
| |
| default: |
| throw new DOMException(DOMException.NOT_SUPPORTED_ERR, |
| "Cannot rename nodes of type " + node.getNodeType()); |
| } |
| } |
| |
| /** |
| * Sets {@code node} to be not namespace-aware and assigns its name. |
| * |
| * @param node an element or attribute node. |
| */ |
| static void setName(NodeImpl node, String name) { |
| int prefixSeparator = name.lastIndexOf(":"); |
| if (prefixSeparator != -1) { |
| String prefix = name.substring(0, prefixSeparator); |
| String localName = name.substring(prefixSeparator + 1); |
| if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) { |
| throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name); |
| } |
| } else if (!DocumentImpl.isXMLIdentifier(name)) { |
| throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name); |
| } |
| |
| switch (node.getNodeType()) { |
| case ATTRIBUTE_NODE: |
| AttrImpl attr = (AttrImpl) node; |
| attr.namespaceAware = false; |
| attr.localName = name; |
| break; |
| |
| case ELEMENT_NODE: |
| ElementImpl element = (ElementImpl) node; |
| element.namespaceAware = false; |
| element.localName = name; |
| break; |
| |
| default: |
| throw new DOMException(DOMException.NOT_SUPPORTED_ERR, |
| "Cannot rename nodes of type " + node.getNodeType()); |
| } |
| } |
| |
| public final String getBaseURI() { |
| switch (getNodeType()) { |
| case DOCUMENT_NODE: |
| return sanitizeUri(((Document) this).getDocumentURI()); |
| |
| case ELEMENT_NODE: |
| Element element = (Element) this; |
| String uri = element.getAttributeNS( |
| "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base" |
| |
| try { |
| // if this node has no base URI, return the parent's. |
| if (uri == null || uri.isEmpty()) { |
| return getParentBaseUri(); |
| } |
| |
| // if this node's URI is absolute, return it |
| if (new URI(uri).isAbsolute()) { |
| return uri; |
| } |
| |
| // this node has a relative URI. Try to resolve it against the |
| // parent, but if that doesn't work just give up and return null. |
| String parentUri = getParentBaseUri(); |
| if (parentUri == null) { |
| return null; |
| } |
| |
| return new URI(parentUri).resolve(uri).toString(); |
| } catch (URISyntaxException e) { |
| return null; |
| } |
| |
| case PROCESSING_INSTRUCTION_NODE: |
| return getParentBaseUri(); |
| |
| case NOTATION_NODE: |
| case ENTITY_NODE: |
| // When we support these node types, the parser should |
| // initialize a base URI field on these nodes. |
| return null; |
| |
| case ENTITY_REFERENCE_NODE: |
| // TODO: get this value from the parser, falling back to the |
| // referenced entity's baseURI if that doesn't exist |
| return null; |
| |
| case DOCUMENT_TYPE_NODE: |
| case DOCUMENT_FRAGMENT_NODE: |
| case ATTRIBUTE_NODE: |
| case TEXT_NODE: |
| case CDATA_SECTION_NODE: |
| case COMMENT_NODE: |
| return null; |
| |
| default: |
| throw new DOMException(DOMException.NOT_SUPPORTED_ERR, |
| "Unsupported node type " + getNodeType()); |
| } |
| } |
| |
| private String getParentBaseUri() { |
| Node parentNode = getParentNode(); |
| return parentNode != null ? parentNode.getBaseURI() : null; |
| } |
| |
| /** |
| * Returns the sanitized input if it is a URI, or {@code null} otherwise. |
| */ |
| private String sanitizeUri(String uri) { |
| if (uri == null || uri.length() == 0) { |
| return null; |
| } |
| try { |
| return new URI(uri).toString(); |
| } catch (URISyntaxException e) { |
| return null; |
| } |
| } |
| |
| public short compareDocumentPosition(Node other) |
| throws DOMException { |
| throw new UnsupportedOperationException(); // TODO |
| } |
| |
| public String getTextContent() throws DOMException { |
| return getNodeValue(); |
| } |
| |
| void getTextContent(StringBuilder buf) throws DOMException { |
| String content = getNodeValue(); |
| if (content != null) { |
| buf.append(content); |
| } |
| } |
| |
| public final void setTextContent(String textContent) throws DOMException { |
| switch (getNodeType()) { |
| case DOCUMENT_TYPE_NODE: |
| case DOCUMENT_NODE: |
| return; // do nothing! |
| |
| case ELEMENT_NODE: |
| case ENTITY_NODE: |
| case ENTITY_REFERENCE_NODE: |
| case DOCUMENT_FRAGMENT_NODE: |
| // remove all existing children |
| Node child; |
| while ((child = getFirstChild()) != null) { |
| removeChild(child); |
| } |
| // create a text node to hold the given content |
| if (textContent != null && textContent.length() != 0) { |
| appendChild(document.createTextNode(textContent)); |
| } |
| return; |
| |
| case ATTRIBUTE_NODE: |
| case TEXT_NODE: |
| case CDATA_SECTION_NODE: |
| case PROCESSING_INSTRUCTION_NODE: |
| case COMMENT_NODE: |
| case NOTATION_NODE: |
| setNodeValue(textContent); |
| return; |
| |
| default: |
| throw new DOMException(DOMException.NOT_SUPPORTED_ERR, |
| "Unsupported node type " + getNodeType()); |
| } |
| } |
| |
| public boolean isSameNode(Node other) { |
| return this == other; |
| } |
| |
| /** |
| * Returns the element whose namespace definitions apply to this node. Use |
| * this element when mapping prefixes to URIs and vice versa. |
| */ |
| private NodeImpl getNamespacingElement() { |
| switch (this.getNodeType()) { |
| case ELEMENT_NODE: |
| return this; |
| |
| case DOCUMENT_NODE: |
| return (NodeImpl) ((Document) this).getDocumentElement(); |
| |
| case ENTITY_NODE: |
| case NOTATION_NODE: |
| case DOCUMENT_FRAGMENT_NODE: |
| case DOCUMENT_TYPE_NODE: |
| return null; |
| |
| case ATTRIBUTE_NODE: |
| return (NodeImpl) ((Attr) this).getOwnerElement(); |
| |
| case TEXT_NODE: |
| case CDATA_SECTION_NODE: |
| case ENTITY_REFERENCE_NODE: |
| case PROCESSING_INSTRUCTION_NODE: |
| case COMMENT_NODE: |
| return getContainingElement(); |
| |
| default: |
| throw new DOMException(DOMException.NOT_SUPPORTED_ERR, |
| "Unsupported node type " + getNodeType()); |
| } |
| } |
| |
| /** |
| * Returns the nearest ancestor element that contains this node. |
| */ |
| private NodeImpl getContainingElement() { |
| for (Node p = getParentNode(); p != null; p = p.getParentNode()) { |
| if (p.getNodeType() == ELEMENT_NODE) { |
| return (NodeImpl) p; |
| } |
| } |
| return null; |
| } |
| |
| public final String lookupPrefix(String namespaceURI) { |
| if (namespaceURI == null) { |
| return null; |
| } |
| |
| // the XML specs define some prefixes (like "xml" and "xmlns") but this |
| // API is explicitly defined to ignore those. |
| |
| NodeImpl target = getNamespacingElement(); |
| for (NodeImpl node = target; node != null; node = node.getContainingElement()) { |
| // check this element's namespace first |
| if (namespaceURI.equals(node.getNamespaceURI()) |
| && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) { |
| return node.getPrefix(); |
| } |
| |
| // search this element for an attribute of this form: |
| // xmlns:foo="http://namespaceURI" |
| if (!node.hasAttributes()) { |
| continue; |
| } |
| NamedNodeMap attributes = node.getAttributes(); |
| for (int i = 0, length = attributes.getLength(); i < length; i++) { |
| Node attr = attributes.item(i); |
| if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI()) |
| || !"xmlns".equals(attr.getPrefix()) |
| || !namespaceURI.equals(attr.getNodeValue())) { |
| continue; |
| } |
| if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) { |
| return attr.getLocalName(); |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns true if the given prefix is mapped to the given URI on this |
| * element. Since child elements can redefine prefixes, this check is |
| * necessary: {@code |
| * <foo xmlns:a="http://good"> |
| * <bar xmlns:a="http://evil"> |
| * <a:baz /> |
| * </bar> |
| * </foo>} |
| * |
| * @param prefix the prefix to find. Nullable. |
| * @param uri the URI to match. Non-null. |
| */ |
| boolean isPrefixMappedToUri(String prefix, String uri) { |
| if (prefix == null) { |
| return false; |
| } |
| |
| String actual = lookupNamespaceURI(prefix); |
| return uri.equals(actual); |
| } |
| |
| public final boolean isDefaultNamespace(String namespaceURI) { |
| String actual = lookupNamespaceURI(null); // null yields the default namespace |
| return namespaceURI == null |
| ? actual == null |
| : namespaceURI.equals(actual); |
| } |
| |
| public final String lookupNamespaceURI(String prefix) { |
| NodeImpl target = getNamespacingElement(); |
| for (NodeImpl node = target; node != null; node = node.getContainingElement()) { |
| // check this element's namespace first |
| String nodePrefix = node.getPrefix(); |
| if (node.getNamespaceURI() != null) { |
| if (prefix == null // null => default prefix |
| ? nodePrefix == null |
| : prefix.equals(nodePrefix)) { |
| return node.getNamespaceURI(); |
| } |
| } |
| |
| // search this element for an attribute of the appropriate form. |
| // default namespace: xmlns="http://resultUri" |
| // non default: xmlns:specifiedPrefix="http://resultUri" |
| if (!node.hasAttributes()) { |
| continue; |
| } |
| NamedNodeMap attributes = node.getAttributes(); |
| for (int i = 0, length = attributes.getLength(); i < length; i++) { |
| Node attr = attributes.item(i); |
| if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) { |
| continue; |
| } |
| if (prefix == null // null => default prefix |
| ? "xmlns".equals(attr.getNodeName()) |
| : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) { |
| String value = attr.getNodeValue(); |
| return value.length() > 0 ? value : null; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns a list of objects such that two nodes are equal if their lists |
| * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes, |
| * neither of which override Object.equals(). Such values must be compared |
| * manually. |
| */ |
| private static List<Object> createEqualityKey(Node node) { |
| List<Object> values = new ArrayList<Object>(); |
| values.add(node.getNodeType()); |
| values.add(node.getNodeName()); |
| values.add(node.getLocalName()); |
| values.add(node.getNamespaceURI()); |
| values.add(node.getPrefix()); |
| values.add(node.getNodeValue()); |
| for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { |
| values.add(child); |
| } |
| |
| switch (node.getNodeType()) { |
| case DOCUMENT_TYPE_NODE: |
| DocumentTypeImpl doctype = (DocumentTypeImpl) node; |
| values.add(doctype.getPublicId()); |
| values.add(doctype.getSystemId()); |
| values.add(doctype.getInternalSubset()); |
| values.add(doctype.getEntities()); |
| values.add(doctype.getNotations()); |
| break; |
| |
| case ELEMENT_NODE: |
| Element element = (Element) node; |
| values.add(element.getAttributes()); |
| break; |
| } |
| |
| return values; |
| } |
| |
| public final boolean isEqualNode(Node arg) { |
| if (arg == this) { |
| return true; |
| } |
| |
| List<Object> listA = createEqualityKey(this); |
| List<Object> listB = createEqualityKey(arg); |
| |
| if (listA.size() != listB.size()) { |
| return false; |
| } |
| |
| for (int i = 0; i < listA.size(); i++) { |
| Object a = listA.get(i); |
| Object b = listB.get(i); |
| |
| if (a == b) { |
| continue; |
| |
| } else if (a == null || b == null) { |
| return false; |
| |
| } else if (a instanceof String || a instanceof Short) { |
| if (!a.equals(b)) { |
| return false; |
| } |
| |
| } else if (a instanceof NamedNodeMap) { |
| if (!(b instanceof NamedNodeMap) |
| || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) { |
| return false; |
| } |
| |
| } else if (a instanceof Node) { |
| if (!(b instanceof Node) |
| || !((Node) a).isEqualNode((Node) b)) { |
| return false; |
| } |
| |
| } else { |
| throw new AssertionError(); // unexpected type |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) { |
| if (a.getLength() != b.getLength()) { |
| return false; |
| } |
| for (int i = 0; i < a.getLength(); i++) { |
| Node aNode = a.item(i); |
| Node bNode = aNode.getLocalName() == null |
| ? b.getNamedItem(aNode.getNodeName()) |
| : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName()); |
| if (bNode == null || !aNode.isEqualNode(bNode)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public final Object getFeature(String feature, String version) { |
| return isSupported(feature, version) ? this : null; |
| } |
| |
| public final Object setUserData(String key, Object data, UserDataHandler handler) { |
| if (key == null) { |
| throw new NullPointerException("key == null"); |
| } |
| Map<String, UserData> map = document.getUserDataMap(this); |
| UserData previous = data == null |
| ? map.remove(key) |
| : map.put(key, new UserData(data, handler)); |
| return previous != null ? previous.value : null; |
| } |
| |
| public final Object getUserData(String key) { |
| if (key == null) { |
| throw new NullPointerException("key == null"); |
| } |
| Map<String, UserData> map = document.getUserDataMapForRead(this); |
| UserData userData = map.get(key); |
| return userData != null ? userData.value : null; |
| } |
| |
| static class UserData { |
| final Object value; |
| final UserDataHandler handler; |
| UserData(Object value, UserDataHandler handler) { |
| this.value = value; |
| this.handler = handler; |
| } |
| } |
| } |