Implementing Document.renameNode() and DOMImplementation.getFeature().

The rename code required moving some behaviour from ElementImpl
and AttrImpl up to their common superclass, NodeImpl.

Change-Id: I30910de146f525a5ecc837895ce5808928b858a0
diff --git a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
index 56a4817..c601de6 100644
--- a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
+++ b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
@@ -36,49 +36,19 @@
 
     // Maintained by ElementImpl.
     ElementImpl ownerElement;
-
-    private boolean namespaceAware;
-
     boolean isId;
-    
-    private String namespaceURI;
 
-    private String localName;
+    boolean namespaceAware;
+    String namespaceURI;
+    String prefix;
+    String localName;
 
-    private String prefix;
-    
     private String value;
 
     AttrImpl(DocumentImpl document, String namespaceURI, String qualifiedName) {
         super(document);
 
-        namespaceAware = true;
-        this.namespaceURI = namespaceURI;
-
-        if (qualifiedName == null || "".equals(qualifiedName)) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
-        }
-        
-        int prefixSeparator = qualifiedName.lastIndexOf(":");
-        if (prefixSeparator != -1) {
-            setPrefix(qualifiedName.substring(0, prefixSeparator));
-            qualifiedName = qualifiedName.substring(prefixSeparator + 1);
-        }
-
-        localName = qualifiedName;
-        
-        if ("".equals(localName)) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, localName);
-        }
-        
-        if ("xmlns".equals(localName) && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, localName);
-        }
-            
-        if (!DocumentImpl.isXMLIdentifier(localName)) {
-            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, localName);
-        }
-            
+        setNameNS(this, namespaceURI, qualifiedName);
         value = "";
     }
 
diff --git a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java
index b662a13..3106c3f 100644
--- a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java
+++ b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java
@@ -85,6 +85,6 @@
     }
 
     public Object getFeature(String feature, String version) {
-        throw new UnsupportedOperationException(); // TODO
+        return hasFeature(feature, version) ? this : null;
     }
 }
diff --git a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java
index c677e58..56283a8 100644
--- a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java
+++ b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java
@@ -299,6 +299,16 @@
         }
     }
 
+    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);
     }
@@ -467,11 +477,6 @@
         ((DOMConfigurationImpl) getDomConfig()).normalize(root);
     }
 
-    public Node renameNode(Node n, String namespaceURI, String qualifiedName) {
-        // TODO: callback the UserDataHandler with a NODE_RENAMED event
-        throw new UnsupportedOperationException(); // TODO
-    }
-
     /**
      * Returns a map with the user data objects attached to the specified node.
      * This map is readable and writable.
diff --git a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
index c3e5a2e..cbc4570 100644
--- a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
+++ b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
@@ -39,37 +39,16 @@
  */
 public class ElementImpl extends InnerNodeImpl implements Element {
 
-    private boolean namespaceAware;
-    
-    private String namespaceURI;
-
-    private String prefix;
-    
-    private String localName;
+    boolean namespaceAware;
+    String namespaceURI;
+    String prefix;
+    String localName;
 
     private List<AttrImpl> attributes = new ArrayList<AttrImpl>();
 
     ElementImpl(DocumentImpl document, String namespaceURI, String qualifiedName) {
         super(document);
-
-        this.namespaceAware = true;
-        this.namespaceURI = namespaceURI;
-
-        if (qualifiedName == null || "".equals(qualifiedName)) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
-        }
-        
-        int p = qualifiedName.lastIndexOf(":");
-        if (p != -1) {
-            setPrefix(qualifiedName.substring(0, p));
-            qualifiedName = qualifiedName.substring(p + 1);
-        }
-        
-        if (!DocumentImpl.isXMLIdentifier(qualifiedName)) {
-            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName);
-        }
-            
-        this.localName = qualifiedName;
+        setNameNS(this, namespaceURI, qualifiedName);
     }
 
     ElementImpl(DocumentImpl document, String name) {
@@ -383,7 +362,7 @@
     public void setPrefix(String prefix) {
         this.prefix = validatePrefix(prefix, namespaceAware, namespaceURI);
     }
-    
+
     public class ElementAttrNamedNodeMapImpl implements NamedNodeMap {
 
         public int getLength() {
diff --git a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
index 359a042..8beb18c 100644
--- a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
+++ b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
@@ -198,7 +198,7 @@
      * @param namespaceAware whether this node is namespace aware
      * @param namespaceURI this node's namespace URI
      */
-    protected String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) {
+    static String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) {
         if (!namespaceAware) {
             throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
         }
@@ -216,6 +216,58 @@
     }
 
     /**
+     * Sets the element or attribute node to be namespace-aware and assign it
+     * the specified name and namespace URI.
+     *
+     * @param node an AttrImpl or ElementImpl 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());
+        }
+    }
+
+    /**
      * 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.
diff --git a/libcore/xml/src/test/java/org/apache/harmony/xml/XsltXPathConformanceTestSuite.java b/libcore/xml/src/test/java/org/apache/harmony/xml/XsltXPathConformanceTestSuite.java
index 7dbfaea..ca356d8 100644
--- a/libcore/xml/src/test/java/org/apache/harmony/xml/XsltXPathConformanceTestSuite.java
+++ b/libcore/xml/src/test/java/org/apache/harmony/xml/XsltXPathConformanceTestSuite.java
@@ -74,7 +74,7 @@
  *         suite zip file from the OASIS project site.</li>
  *     <li>Unzip.
  *     <li>Copy the files to a device: <code>adb shell mkdir /data/oasis ;
- *         adb push ./XSLT-Conformance-TC /data/oasis</code>.
+ *         adb push ./XSLT-Conformance-TC/data/oasis</code>.
  *     <li>Invoke this class' main method, passing the on-device path to the test
  *         suite's <code>catalog.xml</code> file as an argument.
  * </ul>
diff --git a/libcore/xml/src/test/java/tests/xml/DomTest.java b/libcore/xml/src/test/java/tests/xml/DomTest.java
index 3bafb78..2f364d0 100644
--- a/libcore/xml/src/test/java/tests/xml/DomTest.java
+++ b/libcore/xml/src/test/java/tests/xml/DomTest.java
@@ -61,6 +61,7 @@
 import static org.w3c.dom.UserDataHandler.NODE_ADOPTED;
 import static org.w3c.dom.UserDataHandler.NODE_CLONED;
 import static org.w3c.dom.UserDataHandler.NODE_IMPORTED;
+import static org.w3c.dom.UserDataHandler.NODE_RENAMED;
 
 /**
  * Construct a DOM and then interrogate it.
@@ -512,25 +513,25 @@
     }
 
     public void testCoreFeature() {
-        assertTrue(domImplementation.hasFeature("Core", null));
-        assertTrue(domImplementation.hasFeature("Core", ""));
-        assertTrue(domImplementation.hasFeature("Core", "1.0"));
-        assertTrue(domImplementation.hasFeature("Core", "2.0"));
-        assertTrue(domImplementation.hasFeature("Core", "3.0"));
-        assertTrue(domImplementation.hasFeature("CORE", "3.0"));
-        assertTrue(domImplementation.hasFeature("+Core", "3.0"));
-        assertFalse(domImplementation.hasFeature("Core", "4.0"));
+        assertFeature("Core", null);
+        assertFeature("Core", "");
+        assertFeature("Core", "1.0");
+        assertFeature("Core", "2.0");
+        assertFeature("Core", "3.0");
+        assertFeature("CORE", "3.0");
+        assertFeature("+Core", "3.0");
+        assertNoFeature("Core", "4.0");
     }
 
     public void testXmlFeature() {
-        assertTrue(domImplementation.hasFeature("XML", null));
-        assertTrue(domImplementation.hasFeature("XML", ""));
-        assertTrue(domImplementation.hasFeature("XML", "1.0"));
-        assertTrue(domImplementation.hasFeature("XML", "2.0"));
-        assertTrue(domImplementation.hasFeature("XML", "3.0"));
-        assertTrue(domImplementation.hasFeature("Xml", "3.0"));
-        assertTrue(domImplementation.hasFeature("+XML", "3.0"));
-        assertFalse(domImplementation.hasFeature("XML", "4.0"));
+        assertFeature("XML", null);
+        assertFeature("XML", "");
+        assertFeature("XML", "1.0");
+        assertFeature("XML", "2.0");
+        assertFeature("XML", "3.0");
+        assertFeature("Xml", "3.0");
+        assertFeature("+XML", "3.0");
+        assertNoFeature("XML", "4.0");
     }
 
     /**
@@ -538,26 +539,35 @@
      * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#Document3-version
      */
     public void testXmlVersionFeature() {
-        String message = "This implementation does not support the XMLVersion feature";
-        assertTrue(message, domImplementation.hasFeature("XMLVersion", null));
-        assertTrue(message, domImplementation.hasFeature("XMLVersion", ""));
-        assertTrue(message, domImplementation.hasFeature("XMLVersion", "1.0"));
-        assertTrue(message, domImplementation.hasFeature("XMLVersion", "1.1"));
-        assertTrue(message, domImplementation.hasFeature("XMLVERSION", "1.1"));
-        assertTrue(message, domImplementation.hasFeature("+XMLVersion", "1.1"));
-        assertFalse(domImplementation.hasFeature("XMLVersion", "1.2"));
-        assertFalse(domImplementation.hasFeature("XMLVersion", "2.0"));
-        assertFalse(domImplementation.hasFeature("XMLVersion", "2.0"));
+        assertFeature("XMLVersion", null);
+        assertFeature("XMLVersion", "");
+        assertFeature("XMLVersion", "1.0");
+        assertFeature("XMLVersion", "1.1");
+        assertFeature("XMLVERSION", "1.1");
+        assertFeature("+XMLVersion", "1.1");
+        assertNoFeature("XMLVersion", "1.2");
+        assertNoFeature("XMLVersion", "2.0");
+        assertNoFeature("XMLVersion", "2.0");
     }
 
     public void testLsFeature() {
-        assertTrue("This implementation does not support the LS feature",
-                domImplementation.hasFeature("LS", "3.0"));
+        assertFeature("LS", "3.0");
     }
 
     public void testElementTraversalFeature() {
-        assertTrue("This implementation does not support the ElementTraversal feature",
-                domImplementation.hasFeature("ElementTraversal", "1.0"));
+        assertFeature("ElementTraversal", "1.0");
+    }
+
+    private void assertFeature(String feature, String version) {
+        String message = "This implementation is expected to support "
+                + feature + " v. " + version + " but does not.";
+        assertTrue(message, domImplementation.hasFeature(feature, version));
+        assertNotNull(message, domImplementation.getFeature(feature, version));
+    }
+
+    private void assertNoFeature(String feature, String version) {
+        assertFalse(domImplementation.hasFeature(feature, version));
+        assertNull(domImplementation.getFeature(feature, version));
     }
 
     public void testIsSupported() {
@@ -1189,6 +1199,118 @@
         assertFalse(typeInfo.isDerivedFrom("x", "y", TypeInfo.DERIVATION_UNION));
     }
 
+    public void testRenameElement() {
+        document.renameNode(description, null, "desc");
+        assertEquals("desc", description.getTagName());
+        assertEquals("desc", description.getLocalName());
+        assertEquals(null, description.getPrefix());
+        assertEquals(null, description.getNamespaceURI());
+    }
+
+    public void testRenameElementWithPrefix() {
+        try {
+            document.renameNode(description, null, "a:desc");
+            fail();
+        } catch (DOMException e) {
+        }
+    }
+
+    public void testRenameElementWithNamespace() {
+        document.renameNode(description, "http://sales", "desc");
+        assertEquals("desc", description.getTagName());
+        assertEquals("desc", description.getLocalName());
+        assertEquals(null, description.getPrefix());
+        assertEquals("http://sales", description.getNamespaceURI());
+    }
+
+    public void testRenameElementWithPrefixAndNamespace() {
+        document.renameNode(description, "http://sales", "a:desc");
+        assertEquals("a:desc", description.getTagName());
+        assertEquals("desc", description.getLocalName());
+        assertEquals("a", description.getPrefix());
+        assertEquals("http://sales", description.getNamespaceURI());
+    }
+
+    public void testRenameAttribute() {
+        document.renameNode(deluxe, null, "special");
+        assertEquals("special", deluxe.getName());
+        assertEquals("special", deluxe.getLocalName());
+        assertEquals(null, deluxe.getPrefix());
+        assertEquals(null, deluxe.getNamespaceURI());
+    }
+
+    public void testRenameAttributeWithPrefix() {
+        try {
+            document.renameNode(deluxe, null, "a:special");
+            fail();
+        } catch (DOMException e) {
+        }
+    }
+
+    public void testRenameAttributeWithNamespace() {
+        document.renameNode(deluxe, "http://sales", "special");
+        assertEquals("special", deluxe.getName());
+        assertEquals("special", deluxe.getLocalName());
+        assertEquals(null, deluxe.getPrefix());
+        assertEquals("http://sales", deluxe.getNamespaceURI());
+    }
+
+    public void testRenameAttributeWithPrefixAndNamespace() {
+        document.renameNode(deluxe, "http://sales", "a:special");
+        assertEquals("a:special", deluxe.getName());
+        assertEquals("special", deluxe.getLocalName());
+        assertEquals("a", deluxe.getPrefix());
+        assertEquals("http://sales", deluxe.getNamespaceURI());
+    }
+
+    public void testUserDataHandlerNotifiedOfRenames() {
+        RecordingHandler handler = new RecordingHandler();
+        description.setUserData("a", "apple", handler);
+        deluxe.setUserData("b", "banana", handler);
+        standard.setUserData("c", "cat", handler);
+
+        document.renameNode(deluxe, null, "special");
+        document.renameNode(description, null, "desc");
+
+        Set<String> expected = new HashSet<String>();
+        expected.add(notification(NODE_RENAMED, "a", "apple", description, null));
+        expected.add(notification(NODE_RENAMED, "b", "banana", deluxe, null));
+        assertEquals(expected, handler.calls);
+    }
+
+    public void testRenameToInvalid() {
+        try {
+            document.renameNode(description, null, "xmlns:foo");
+            fail();
+        } catch (DOMException e) {
+        }
+        try {
+            document.renameNode(description, null, "xml:foo");
+            fail();
+        } catch (DOMException e) {
+        }
+        try {
+            document.renameNode(deluxe, null, "xmlns");
+            fail();
+        } catch (DOMException e) {
+        }
+    }
+
+    public void testRenameNodeOtherThanElementOrAttribute() {
+        for (Node node : allNodes) {
+            if (node.getNodeType() == Node.ATTRIBUTE_NODE
+                    || node.getNodeType() == Node.ELEMENT_NODE) {
+                continue;
+            }
+
+            try {
+                document.renameNode(node, null, "foo");
+                fail();
+            } catch (DOMException e) {
+            }
+        }
+    }
+
     private class RecordingHandler implements UserDataHandler {
         final Set<String> calls = new HashSet<String>();
         public void handle(short operation, String key, Object data, Node src, Node dst) {