Filling in some gaps in our XML DOM v3 API.

Specifically, these methods on Node:
 - setTextContent()
 - isSameNode()
 - lookupPrefix()
 - lookupNamespaceURI()
In order to implement the last 2 I needed to fix our KXml parser
to include namespace attributes (ie. xmlns) in the pulled document.
Previously these were being elided.

Added a new testcase to verify our behaviour. It passes the RI. On
Dalvik we have a small issue with entity declarations.

Added a new testcase to verify Node.getBaseURI(). This test fails
because the method isn't implemented. Part of this test required
moving a method out to Support_Resources.java; in order to verify
the BaseURI the XML must be read from a file and not a stream (so
that path information exists).

Also...
 - Style cleanup: changing static calls to look like static calls.
 - Efficiency: avoiding concatenating with "" when unnecessary
 - Duplication: sharing prefix validation between attributes and elements
 - Renaming NodeTests to NodeTest for vogar-friendliness

Outstanding:
 - I need to write a test for setTextContent().
diff --git a/support/src/test/java/tests/support/resource/Support_Resources.java b/support/src/test/java/tests/support/resource/Support_Resources.java
index 53a8925..67b6001 100644
--- a/support/src/test/java/tests/support/resource/Support_Resources.java
+++ b/support/src/test/java/tests/support/resource/Support_Resources.java
@@ -205,4 +205,20 @@
             throw new RuntimeException("Failed to load resource: " + name);
         }
     }
+
+    public static File resourceToTempFile(String path) throws IOException {
+        File f = File.createTempFile("out", ".xml");
+        f.deleteOnExit();
+        FileOutputStream out = new FileOutputStream(f);
+
+        InputStream xml = Support_Resources.class.getResourceAsStream(path);
+        int b;
+        while ((b = xml.read()) != -1) {
+            out.write(b);
+        }
+        out.flush();
+        out.close();
+        xml.close();
+        return f;
+    }
 }
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
index 59a8b78..c071899 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
@@ -73,7 +73,7 @@
             throw new DOMException(DOMException.NAMESPACE_ERR, localName);
         }
             
-        if (!document.isXMLIdentifier(localName)) {
+        if (!DocumentImpl.isXMLIdentifier(localName)) {
             throw new DOMException(DOMException.INVALID_CHARACTER_ERR, localName);
         }
             
@@ -90,11 +90,11 @@
             String prefix = name.substring(0, prefixSeparator);
             String localName = name.substring(prefixSeparator + 1);
             
-            if (!document.isXMLIdentifier(prefix) || !document.isXMLIdentifier(localName)) {
+            if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         } else {
-            if (!document.isXMLIdentifier(name)) {
+            if (!DocumentImpl.isXMLIdentifier(name)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         }
@@ -108,7 +108,9 @@
     }
 
     public String getName() {
-        return (prefix != null ? prefix + ":" : "") + localName;
+        return prefix != null
+                ? prefix + ":" + localName
+                : localName;
     }
 
     @Override
@@ -154,22 +156,7 @@
     
     @Override
     public void setPrefix(String prefix) {
-        if (!namespaceAware) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-        }
-        
-        if (prefix != null) {
-            if (namespaceURI == null
-                    || !DocumentImpl.isXMLIdentifier(prefix)
-                    || ("xmlns".equals(prefix)
-                            && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI))
-                    || ("xml".equals(prefix)
-                            && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI))) {
-                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-            }
-        }
-
-        this.prefix = prefix;
+        this.prefix = validatePrefix(prefix, namespaceAware, namespaceURI);
     }
     
     public void setValue(String value) throws DOMException {
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
index 230e444..df1383d 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
@@ -65,7 +65,7 @@
             qualifiedName = qualifiedName.substring(p + 1);
         }
         
-        if (!document.isXMLIdentifier(qualifiedName)) {
+        if (!DocumentImpl.isXMLIdentifier(qualifiedName)) {
             throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName);
         }
             
@@ -82,11 +82,11 @@
             String prefix = name.substring(0, p);
             String localName = name.substring(p + 1);
             
-            if (!document.isXMLIdentifier(prefix) || !document.isXMLIdentifier(localName)) {
+            if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         } else {
-            if (!document.isXMLIdentifier(name)) {
+            if (!DocumentImpl.isXMLIdentifier(name)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         }
@@ -241,7 +241,9 @@
     }
 
     public String getTagName() {
-        return (prefix != null ? prefix + ":" : "") + localName;
+        return prefix != null
+                ? prefix + ":" + localName
+                : localName;
     }
 
     public boolean hasAttribute(String name) {
@@ -281,7 +283,7 @@
             throw new DOMException(DOMException.NOT_FOUND_ERR, null);
         }
 
-        attributes.remove(oldAttr);
+        attributes.remove(oldAttrImpl);
         oldAttrImpl.ownerElement = null;
 
         return oldAttrImpl;
@@ -362,21 +364,7 @@
 
     @Override
     public void setPrefix(String prefix) {
-        if (!namespaceAware) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-        }
-        
-        if (prefix != null) {
-            if (namespaceURI == null || !document.isXMLIdentifier(prefix)) {
-                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-            }
-            
-            if ("xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI)) {
-                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-            }
-        }
-        
-        this.prefix = prefix;
+        this.prefix = validatePrefix(prefix, namespaceAware, namespaceURI);
     }
     
     public class ElementAttrNamedNodeMapImpl implements NamedNodeMap {
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
index b752506..57ff7dc 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
@@ -16,6 +16,7 @@
 
 package org.apache.harmony.xml.dom;
 
+import org.w3c.dom.Attr;
 import org.w3c.dom.DOMException;
 import org.w3c.dom.Document;
 import org.w3c.dom.NamedNodeMap;
@@ -24,16 +25,15 @@
 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.
- * <p>
- * This class represents a Node that has neither a parent nor children.
+ * 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 {
 
@@ -142,6 +142,29 @@
     }
 
     /**
+     * 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
+     */
+    protected 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;
+    }
+
+    /**
      * Checks whether a required string matches an actual string. This utility
      * method is used for comparing namespaces and such. It takes into account
      * null arguments and the "*" special case.
@@ -190,7 +213,34 @@
     }
 
     public String getBaseURI() {
-        return null; // TODO
+        /*
+         * TODO: implement. For reference, here's Xerces' behaviour:
+         *
+         * In all cases, the returned URI should be sanitized before it is
+         * returned. If the URI is malformed, null should be returned instead.
+         *
+         * For document nodes, this should return a member field that's
+         * initialized by the parser.
+         *
+         * For element nodes, this should first look for the xml:base attribute.
+         *   if that exists and is absolute, it should be returned.
+         *   if that exists and is relative, it should be resolved to the parent's base URI
+         *   if it doesn't exist, the parent's baseURI should be returned
+         *
+         * For entity nodes, if a base URI exists that should be returned.
+         * Otherwise the document's base URI should be returned
+         *
+         * For entity references, if a base URI exists that should be returned
+         * otherwise it dereferences the entity (via the document) and uses the
+         * entity's base URI.
+         *
+         * For notations, it returns the base URI field.
+         *
+         * For processing instructions, it returns the parent's base URI.
+         *
+         * For all other node types, it returns null.
+         */
+        return null;
     }
 
     public short compareDocumentPosition(Node other)
@@ -210,23 +260,190 @@
     }
 
     public void setTextContent(String textContent) throws DOMException {
-        throw new UnsupportedOperationException(); // TODO
+        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(getOwnerDocument().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) {
-        throw new UnsupportedOperationException(); // TODO
+        return this == other;
     }
 
-    public String lookupPrefix(String namespaceURI) {
-        throw new UnsupportedOperationException(); // TODO
+    /**
+     * 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 boolean isDefaultNamespace(String namespaceURI) {
         throw new UnsupportedOperationException(); // TODO
     }
 
-    public String lookupNamespaceURI(String prefix) {
-        throw new UnsupportedOperationException(); // TODO
+    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;
     }
 
     public boolean isEqualNode(Node arg) {
diff --git a/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java b/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java
index 5a3c48c..52240aa 100644
--- a/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java
@@ -115,8 +115,8 @@
         Document document = newDocument();
 
         try {
-            XmlPullParser parser = new KXmlParser();
-
+            KXmlParser parser = new KXmlParser();
+            parser.keepNamespaceAttributes();
             parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
                     namespaceAware);
             
diff --git a/xml/src/main/java/org/kxml2/io/KXmlParser.java b/xml/src/main/java/org/kxml2/io/KXmlParser.java
index c4d8f3d..99eb03b 100644
--- a/xml/src/main/java/org/kxml2/io/KXmlParser.java
+++ b/xml/src/main/java/org/kxml2/io/KXmlParser.java
@@ -45,6 +45,7 @@
 
     private boolean processNsp;
     private boolean relaxed;
+    private boolean keepNamespaceAttributes; // android-added
     private Hashtable entityMap;
     private int depth;
     private String[] elementStack = new String[16];
@@ -80,6 +81,14 @@
 
     private boolean degenerated;
     private int attributeCount;
+
+    /**
+     * The current element's attributes arranged in groups of 4:
+     * i + 0 = attribute namespace URI
+     * i + 1 = attribute namespace prefix
+     * i + 2 = attribute qualified name (may contain ":", as in "html:h1")
+     * i + 3 = attribute value
+     */
     private String[] attributes = new String[16];
 //    private int stackMismatch = 0;
     private String error;
@@ -100,6 +109,19 @@
             new char[Runtime.getRuntime().freeMemory() >= 1048576 ? 8192 : 128];
     }
 
+    // BEGIN android-added
+    /**
+     * Retains namespace attributes like {@code xmlns="http://foo"} or {@code
+     * xmlns:foo="http:foo"} in pulled elements. Most applications will only be
+     * interested in the effective namespaces of their elements, so these
+     * attributes aren't useful. But for structure preserving wrappers like DOM,
+     * it is necessary to keep the namespace data around.
+     */
+    public void keepNamespaceAttributes() {
+        this.keepNamespaceAttributes = true;
+    }
+    // END android-added
+
     private final boolean isProp(String n1, boolean prop, String n2) {
         if (!n1.startsWith("http://xmlpull.org/v1/doc/"))
             return false;
@@ -148,14 +170,23 @@
 
                 //System.out.println (prefixMap);
 
-                System.arraycopy(
-                    attributes,
-                    i + 4,
-                    attributes,
-                    i,
-                    ((--attributeCount) << 2) - i);
+                // BEGIN android-changed
+                if (keepNamespaceAttributes) {
+                    // explicitly set the namespace for unprefixed attributes 
+                    // such as xmlns="http://foo"
+                    attributes[i] = "http://www.w3.org/2000/xmlns/";
+                    any = true;
+                } else {
+                    System.arraycopy(
+                            attributes,
+                            i + 4,
+                            attributes,
+                            i,
+                            ((--attributeCount) << 2) - i);
 
-                i -= 4;
+                    i -= 4;
+                }
+                // END android-changed
             }
         }
 
diff --git a/xml/src/main/java/org/w3c/dom/Attr.java b/xml/src/main/java/org/w3c/dom/Attr.java
index d9ed6ff..bd7267b 100644
--- a/xml/src/main/java/org/w3c/dom/Attr.java
+++ b/xml/src/main/java/org/w3c/dom/Attr.java
@@ -176,7 +176,7 @@
     /**
      * On retrieval, the value of the attribute is returned as a string. 
      * Character and general entity references are replaced with their 
-     * values. See also the method <code>getAttribute</code> on the 
+     * values. See also the method <code>getAttribute</code> on the
      * <code>Element</code> interface.
      * <br>On setting, this creates a <code>Text</code> node with the unparsed 
      * contents of the string, i.e. any characters that an XML processor 
diff --git a/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java b/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java
index 02b6d80..6b10a0d 100644
--- a/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java
+++ b/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java
@@ -34,6 +34,7 @@
 import tests.api.org.xml.sax.support.MethodLogger;
 import tests.api.org.xml.sax.support.MockHandler;
 import tests.api.org.xml.sax.support.MockResolver;
+import tests.support.resource.Support_Resources;
 import tests.util.TestEnvironment;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -41,8 +42,6 @@
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 
@@ -272,7 +271,7 @@
         args = {java.io.File.class}
     )
     public void testGetBaseURI() throws IOException, SAXException {
-        File f = resourceToTmpFile("/simple.xml");
+        File f = Support_Resources.resourceToTempFile("/simple.xml");
         Document d = db.parse(f);
         assertTrue(d.getDocumentElement().getBaseURI().startsWith("file://"));
     }
@@ -291,7 +290,7 @@
         args = {java.io.File.class}
     )
     public void test_parseLjava_io_File() throws IOException {
-        File f = resourceToTmpFile("/simple.xml");
+        File f = Support_Resources.resourceToTempFile("/simple.xml");
 
         // case 1: Trivial use.
         try {
@@ -333,7 +332,7 @@
         }
 
         // case 4: Try to parse incorrect xml file
-        f = resourceToTmpFile("/wrong.xml");
+        f = Support_Resources.resourceToTempFile("/wrong.xml");
         try {
             db.parse(f);
             fail("Expected SAXException was not thrown");
@@ -344,22 +343,6 @@
         }
     }
 
-    private File resourceToTmpFile(String path) throws IOException,
-            FileNotFoundException {
-        File f = File.createTempFile("out", ".xml");
-        f.deleteOnExit();
-        FileOutputStream out = new FileOutputStream(f);
-
-        InputStream xml = getClass().getResourceAsStream(path);
-        while (xml.available() > 0) {
-            out.write(xml.read());
-        }
-        out.flush();
-        out.close();
-        xml.close();
-        return f;
-    }
-
     /**
      * @tests javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream)
      * Case 1: Try to parse correct xml document.
diff --git a/xml/src/test/java/tests/xml/AllTests.java b/xml/src/test/java/tests/xml/AllTests.java
index 45ca18e..89da364 100644
--- a/xml/src/test/java/tests/xml/AllTests.java
+++ b/xml/src/test/java/tests/xml/AllTests.java
@@ -26,7 +26,7 @@
 
         suite.addTestSuite(SimpleParserTest.class);
         suite.addTestSuite(SimpleBuilderTest.class);
-        suite.addTestSuite(NodeTests.class);
+        suite.addTestSuite(NodeTest.class);
         
         //suite.addTest(tests.org.w3c.dom.AllTests.suite());
         suite.addTest(tests.api.javax.xml.parsers.AllTests.suite());
diff --git a/xml/src/test/java/tests/xml/DomTest.java b/xml/src/test/java/tests/xml/DomTest.java
new file mode 100644
index 0000000..d6031ed
--- /dev/null
+++ b/xml/src/test/java/tests/xml/DomTest.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2010 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 tests.xml;
+
+import junit.framework.TestCase;
+import org.w3c.dom.Attr;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.Comment;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Entity;
+import org.w3c.dom.Node;
+import org.w3c.dom.ProcessingInstruction;
+import org.w3c.dom.Text;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Construct a DOM and then interrogate it.
+ */
+public class DomTest extends TestCase {
+
+    private final String xml
+            = "<!DOCTYPE menu ["
+            + "  <!ENTITY sp \"Maple Syrup\">"
+            + "]>"
+            + "<menu>\n"
+            + "  <item xmlns=\"http://food\" xmlns:a=\"http://addons\">\n"
+            + "    <name a:standard=\"strawberry\" deluxe=\"&sp;\">Waffles</name>\n"
+            + "    <description xmlns=\"http://marketing\">Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)</description>\n"
+            + "    <a:option>Whipped Cream</a:option>\n"
+            + "    <a:option>&sp;</a:option>\n"
+            + "    <?wafflemaker square shape?>\n"
+            + "    <nutrition>\n"
+            + "      <a:vitamins xmlns:a=\"http://usda\">\n"
+            + "        <!-- add other vitamins? --> \n"
+            + "        <a:vitaminc>60%</a:vitaminc>\n"
+            + "      </a:vitamins>\n"
+            + "    </nutrition>\n"
+            + "  </item>\n"
+            + "</menu>";
+
+    private Document document;
+    private DocumentType doctype;
+    private Entity sp;
+    private Element menu;
+    private Element item;
+    private Attr itemXmlns;
+    private Attr itemXmlnsA;
+    private Element name;
+    private Attr standard;
+    private Attr deluxe;
+    private Element description;
+    private Text descriptionText1;
+    private CDATASection descriptionText2;
+    private Text descriptionText3;
+    private Element option1;
+    private Element option2;
+    private Node option2Reference; // Text on RI, EntityReference on Dalvik
+    private ProcessingInstruction wafflemaker;
+    private Element nutrition;
+    private Element vitamins;
+    private Attr vitaminsXmlnsA;
+    private Comment comment;
+    private Element vitaminc;
+    private Text vitamincText;
+
+    private List<Node> allNodes;
+
+    @Override protected void setUp() throws Exception {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+
+        document = factory.newDocumentBuilder()
+                .parse(new InputSource(new StringReader(xml)));
+
+        // doctype nodes
+        doctype = document.getDoctype();
+        if (doctype.getEntities() != null) {
+            sp = (Entity) doctype.getEntities().item(0);
+        }
+
+        // document nodes
+        menu = document.getDocumentElement();
+        item = (Element) menu.getChildNodes().item(1);
+        itemXmlns = item.getAttributeNode("xmlns");
+        itemXmlnsA = item.getAttributeNode("xmlns:a");
+        name = (Element) item.getChildNodes().item(1);
+        standard = name.getAttributeNode("a:standard");
+        deluxe = name.getAttributeNode("deluxe");
+        description = (Element) item.getChildNodes().item(3);
+        descriptionText1 = (Text) description.getChildNodes().item(0);
+        descriptionText2 = (CDATASection) description.getChildNodes().item(1);
+        descriptionText3 = (Text) description.getChildNodes().item(2);
+        option1 = (Element) item.getChildNodes().item(5);
+        option2 = (Element) item.getChildNodes().item(7);
+        option2Reference = option2.getChildNodes().item(0);
+        wafflemaker = (ProcessingInstruction) item.getChildNodes().item(9);
+        nutrition = (Element) item.getChildNodes().item(11);
+        vitamins = (Element) nutrition.getChildNodes().item(1);
+        vitaminsXmlnsA = vitamins.getAttributeNode("xmlns:a");
+        comment = (Comment) vitamins.getChildNodes().item(1);
+        vitaminc = (Element) vitamins.getChildNodes().item(3);
+        vitamincText = (Text) vitaminc.getChildNodes().item(0);
+
+        allNodes = new ArrayList<Node>();
+
+        if (sp != null) {
+            allNodes.add(sp);
+        }
+
+        allNodes.addAll(Arrays.asList(document, doctype, menu, item, itemXmlns,
+                itemXmlnsA, name, standard, deluxe, description,
+                descriptionText1, descriptionText2, descriptionText3, option1,
+                option2, option2Reference, wafflemaker, nutrition, vitamins,
+                vitaminsXmlnsA, comment, vitaminc, vitamincText));
+    }
+
+    /**
+     * Android's parsed DOM doesn't include entity declarations. These nodes will
+     * only be tested for implementations that support them.
+     */
+    public void testEntityDeclarations() {
+        assertNotNull("This implementation does not parse entity declarations", sp);
+    }
+
+    public void testLookupNamespaceURIByPrefix() {
+        assertEquals(null, doctype.lookupNamespaceURI("a"));
+        if (sp != null) {
+            assertEquals(null, sp.lookupNamespaceURI("a"));
+        }
+        assertEquals(null, document.lookupNamespaceURI("a"));
+        assertEquals(null, menu.lookupNamespaceURI("a"));
+        assertEquals("http://addons", item.lookupNamespaceURI("a"));
+        assertEquals("http://addons", itemXmlns.lookupNamespaceURI("a"));
+        assertEquals("http://addons", itemXmlnsA.lookupNamespaceURI("a"));
+        assertEquals("http://addons", name.lookupNamespaceURI("a"));
+        assertEquals("http://addons", standard.lookupNamespaceURI("a"));
+        assertEquals("http://addons", deluxe.lookupNamespaceURI("a"));
+        assertEquals("http://addons", description.lookupNamespaceURI("a"));
+        assertEquals("http://addons", descriptionText1.lookupNamespaceURI("a"));
+        assertEquals("http://addons", descriptionText2.lookupNamespaceURI("a"));
+        assertEquals("http://addons", descriptionText3.lookupNamespaceURI("a"));
+        assertEquals("http://addons", option1.lookupNamespaceURI("a"));
+        assertEquals("http://addons", option2.lookupNamespaceURI("a"));
+        assertEquals("http://addons", option2Reference.lookupNamespaceURI("a"));
+        assertEquals("http://addons", wafflemaker.lookupNamespaceURI("a"));
+        assertEquals("http://addons", nutrition.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitamins.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitaminsXmlnsA.lookupNamespaceURI("a"));
+        assertEquals("http://usda", comment.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitaminc.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitamincText.lookupNamespaceURI("a"));
+    }
+
+    public void testLookupNamespaceURIWithNullPrefix() {
+        assertEquals(null, document.lookupNamespaceURI(null));
+        assertEquals(null, doctype.lookupNamespaceURI(null));
+        if (sp != null) {
+            assertEquals(null, sp.lookupNamespaceURI(null));
+        }
+        assertEquals(null, menu.lookupNamespaceURI(null));
+        assertEquals("http://food", item.lookupNamespaceURI(null));
+        assertEquals("http://food", itemXmlns.lookupNamespaceURI(null));
+        assertEquals("http://food", itemXmlnsA.lookupNamespaceURI(null));
+        assertEquals("http://food", name.lookupNamespaceURI(null));
+        assertEquals("http://food", standard.lookupNamespaceURI(null));
+        assertEquals("http://food", deluxe.lookupNamespaceURI(null));
+        assertEquals("http://marketing", description.lookupNamespaceURI(null));
+        assertEquals("http://marketing", descriptionText1.lookupNamespaceURI(null));
+        assertEquals("http://marketing", descriptionText2.lookupNamespaceURI(null));
+        assertEquals("http://marketing", descriptionText3.lookupNamespaceURI(null));
+        assertEquals("http://food", option1.lookupNamespaceURI(null));
+        assertEquals("http://food", option2.lookupNamespaceURI(null));
+        assertEquals("http://food", option2Reference.lookupNamespaceURI(null));
+        assertEquals("http://food", wafflemaker.lookupNamespaceURI(null));
+        assertEquals("http://food", nutrition.lookupNamespaceURI(null));
+        assertEquals("http://food", vitamins.lookupNamespaceURI(null));
+        assertEquals("http://food", vitaminsXmlnsA.lookupNamespaceURI(null));
+        assertEquals("http://food", comment.lookupNamespaceURI(null));
+        assertEquals("http://food", vitaminc.lookupNamespaceURI(null));
+        assertEquals("http://food", vitamincText.lookupNamespaceURI(null));
+    }
+
+    public void testLookupNamespaceURIWithXmlnsPrefix() {
+        for (Node node : allNodes) {
+            assertEquals(null, node.lookupNamespaceURI("xmlns"));
+        }
+    }
+
+    public void testLookupPrefixWithShadowedUri() {
+        assertEquals(null, document.lookupPrefix("http://addons"));
+        assertEquals(null, doctype.lookupPrefix("http://addons"));
+        if (sp != null) {
+            assertEquals(null, sp.lookupPrefix("http://addons"));
+        }
+        assertEquals(null, menu.lookupPrefix("http://addons"));
+        assertEquals("a", item.lookupPrefix("http://addons"));
+        assertEquals("a", itemXmlns.lookupPrefix("http://addons"));
+        assertEquals("a", itemXmlnsA.lookupPrefix("http://addons"));
+        assertEquals("a", name.lookupPrefix("http://addons"));
+        assertEquals("a", standard.lookupPrefix("http://addons"));
+        assertEquals("a", deluxe.lookupPrefix("http://addons"));
+        assertEquals("a", description.lookupPrefix("http://addons"));
+        assertEquals("a", descriptionText1.lookupPrefix("http://addons"));
+        assertEquals("a", descriptionText2.lookupPrefix("http://addons"));
+        assertEquals("a", descriptionText3.lookupPrefix("http://addons"));
+        assertEquals("a", option1.lookupPrefix("http://addons"));
+        assertEquals("a", option2.lookupPrefix("http://addons"));
+        assertEquals("a", option2Reference.lookupPrefix("http://addons"));
+        assertEquals("a", wafflemaker.lookupPrefix("http://addons"));
+        assertEquals("a", nutrition.lookupPrefix("http://addons"));
+        assertEquals(null, vitamins.lookupPrefix("http://addons"));
+        assertEquals(null, vitaminsXmlnsA.lookupPrefix("http://addons"));
+        assertEquals(null, comment.lookupPrefix("http://addons"));
+        assertEquals(null, vitaminc.lookupPrefix("http://addons"));
+        assertEquals(null, vitamincText.lookupPrefix("http://addons"));
+    }
+
+    public void testLookupPrefixWithUnusedUri() {
+        for (Node node : allNodes) {
+            assertEquals(null, node.lookupPrefix("http://unused"));
+        }
+    }
+
+    public void testLookupPrefixWithNullUri() {
+        for (Node node : allNodes) {
+            assertEquals(null, node.lookupPrefix(null));
+        }
+    }
+
+    public void testLookupPrefixWithShadowingUri() {
+        assertEquals(null, document.lookupPrefix("http://usda"));
+        assertEquals(null, doctype.lookupPrefix("http://usda"));
+        if (sp != null) {
+            assertEquals(null, sp.lookupPrefix("http://usda"));
+        }
+        assertEquals(null, menu.lookupPrefix("http://usda"));
+        assertEquals(null, item.lookupPrefix("http://usda"));
+        assertEquals(null, itemXmlns.lookupPrefix("http://usda"));
+        assertEquals(null, itemXmlnsA.lookupPrefix("http://usda"));
+        assertEquals(null, name.lookupPrefix("http://usda"));
+        assertEquals(null, standard.lookupPrefix("http://usda"));
+        assertEquals(null, deluxe.lookupPrefix("http://usda"));
+        assertEquals(null, description.lookupPrefix("http://usda"));
+        assertEquals(null, descriptionText1.lookupPrefix("http://usda"));
+        assertEquals(null, descriptionText2.lookupPrefix("http://usda"));
+        assertEquals(null, descriptionText3.lookupPrefix("http://usda"));
+        assertEquals(null, option1.lookupPrefix("http://usda"));
+        assertEquals(null, option2.lookupPrefix("http://usda"));
+        assertEquals(null, option2Reference.lookupPrefix("http://usda"));
+        assertEquals(null, wafflemaker.lookupPrefix("http://usda"));
+        assertEquals(null, nutrition.lookupPrefix("http://usda"));
+        assertEquals("a", vitamins.lookupPrefix("http://usda"));
+        assertEquals("a", vitaminsXmlnsA.lookupPrefix("http://usda"));
+        assertEquals("a", comment.lookupPrefix("http://usda"));
+        assertEquals("a", vitaminc.lookupPrefix("http://usda"));
+        assertEquals("a", vitamincText.lookupPrefix("http://usda"));
+    }
+}
diff --git a/xml/src/test/java/tests/xml/NodeTest.java b/xml/src/test/java/tests/xml/NodeTest.java
new file mode 100644
index 0000000..dc3a333
--- /dev/null
+++ b/xml/src/test/java/tests/xml/NodeTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2009 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 tests.xml;
+
+import dalvik.annotation.TestTargetClass;
+import junit.framework.TestCase;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import tests.support.resource.Support_Resources;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+@TestTargetClass(Node.class)
+public class NodeTest extends TestCase {
+
+    /**
+     * For bug 779: Node#getNextSibling throws IndexOutOfBoundsException.
+     */
+    public void test_getNextSibling() throws Exception {
+        // Calling getNextSibling when there is no next sibling should return null.
+        // From http://code.google.com/p/android/issues/detail?id=779.
+        ByteArrayInputStream bis = new ByteArrayInputStream("<root/>".getBytes());
+        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(bis);
+        Node root = document.getDocumentElement();
+        assertNull(root.getNextSibling());
+    }
+
+    public void testGetBaseUri() throws Exception {
+        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        File file = Support_Resources.resourceToTempFile("/simple.xml");
+        Document document = builder.parse(file);
+
+        String baseUri = "file:" + file.getPath();
+        assertEquals(baseUri, document.getBaseURI());
+
+        Element documentElement = document.getDocumentElement();
+        for (Node node : flattenSubtree(documentElement)) {
+            if (node.getNodeType() == Node.ELEMENT_NODE
+                    || node.getNodeType() == Node.DOCUMENT_NODE) {
+                assertEquals("Unexpected base URI for " + node, baseUri, node.getBaseURI());
+            } else {
+                assertNull("Unexpected base URI for " + node, node.getBaseURI());
+            }
+        }
+
+        // TODO: test other node types
+        // TODO: test resolution of relative paths
+        // TODO: test URI santization
+    }
+
+    private List<Node> flattenSubtree(Node subtree) {
+        List<Node> result = new ArrayList<Node>();
+        traverse(subtree, result);
+        return result;
+    }
+
+    private void traverse(Node node, List<Node> sink) {
+        sink.add(node);
+
+        NodeList children = node.getChildNodes();
+        for (int i = 0; i < children.getLength(); i++) {
+            traverse(children.item(i), sink);
+        }
+    }
+}
diff --git a/xml/src/test/java/tests/xml/NodeTests.java b/xml/src/test/java/tests/xml/NodeTests.java
deleted file mode 100644
index e46e216..0000000
--- a/xml/src/test/java/tests/xml/NodeTests.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2009 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 tests.xml;
-
-import dalvik.annotation.TestLevel;
-import dalvik.annotation.TestTargetNew;
-import dalvik.annotation.TestTargetClass;
-
-import junit.framework.TestCase;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-
-import java.io.ByteArrayInputStream;
-import javax.xml.parsers.DocumentBuilderFactory;
-
-@TestTargetClass(Node.class)
-public class NodeTests extends TestCase {
-    @TestTargetNew(
-        level = TestLevel.PARTIAL,
-        notes = "Issue #779: org.w3c.dom.Node#getNextSibling throws IndexOutOfBoundsException.",
-        method = "getNextSibling",
-        args = {}
-    )
-    public void test_getNextSibling() throws Exception {
-        // Calling getNextSibling when there is no next sibling should return null.
-        // From http://code.google.com/p/android/issues/detail?id=779.
-        ByteArrayInputStream bis = new ByteArrayInputStream("<root/>".getBytes());
-        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(bis);
-        Node root = document.getDocumentElement();
-        assertNull(root.getNextSibling());
-    }
-}