/*
 * 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 dalvik.annotation.KnownFailure;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
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.Entity;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Notation;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.w3c.dom.TypeInfo;
import org.w3c.dom.UserDataHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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.
 */
public class DomTest extends TestCase {

    private Transformer transformer;
    private DocumentBuilder builder;
    private DOMImplementation domImplementation;

    private final String xml
            = "<!DOCTYPE menu ["
            + "  <!ENTITY sp \"Maple Syrup\">"
            + "  <!NOTATION png SYSTEM \"image/png\">"
            + "]>"
            + "<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 Notation png;
    private Element menu;
    private Element item;
    private Attr itemXmlns;
    private Attr itemXmlnsA;
    private Element name;
    private Attr standard;
    private Attr deluxe;
    private Text waffles;
    private Element description;
    private Text descriptionText1;
    private CDATASection descriptionText2;
    private Text descriptionText3;
    private Element option1;
    private Element option2;
    private Node option2Reference; // resolved to Text on RI, an 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 {
        transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        builder = factory.newDocumentBuilder();
        domImplementation = builder.getDOMImplementation();
        document = builder.parse(new InputSource(new StringReader(xml)));

        // doctype nodes
        doctype = document.getDoctype();
        if (doctype.getEntities() != null) {
            sp = (Entity) doctype.getEntities().item(0);
        }
        if (doctype.getNotations() != null) {
            png = (Notation) doctype.getNotations().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");
        waffles = (Text) name.getChildNodes().item(0);
        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);
        }
        if (png != null) {
            allNodes.add(png);
        }

        allNodes.addAll(Arrays.asList(document, doctype, menu, item, itemXmlns,
                itemXmlnsA, name, standard, deluxe, waffles, 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.
     */
    @KnownFailure("Dalvik doesn't parse entity declarations")
    public void testEntityDeclarations() {
        assertNotNull("This implementation does not parse entity declarations", sp);
    }

    /**
     * Android's parsed DOM doesn't include notations. These nodes will only be
     * tested for implementations that support them.
     */
    @KnownFailure("Dalvik doesn't parse notations")
    public void testNotations() {
        assertNotNull("This implementation does not parse notations", png);
    }

    public void testLookupNamespaceURIByPrefix() {
        assertEquals(null, doctype.lookupNamespaceURI("a"));
        if (sp != null) {
            assertEquals(null, sp.lookupNamespaceURI("a"));
        }
        if (png != null) {
            assertEquals(null, png.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));
        }
        if (png != null) {
            assertEquals(null, png.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"));
        }
        if (png != null) {
            assertEquals(null, png.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"));
        }
        if (png != null) {
            assertEquals(null, png.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"));
    }

    public void testIsDefaultNamespace() {
        assertFalse(document.isDefaultNamespace("http://food"));
        assertFalse(doctype.isDefaultNamespace("http://food"));
        if (sp != null) {
            assertFalse(sp.isDefaultNamespace("http://food"));
        }
        if (png != null) {
            assertFalse(png.isDefaultNamespace("http://food"));
        }
        assertFalse(menu.isDefaultNamespace("http://food"));
        assertTrue(item.isDefaultNamespace("http://food"));
        assertTrue(itemXmlns.isDefaultNamespace("http://food"));
        assertTrue(itemXmlnsA.isDefaultNamespace("http://food"));
        assertTrue(name.isDefaultNamespace("http://food"));
        assertTrue(standard.isDefaultNamespace("http://food"));
        assertTrue(deluxe.isDefaultNamespace("http://food"));
        assertFalse(description.isDefaultNamespace("http://food"));
        assertFalse(descriptionText1.isDefaultNamespace("http://food"));
        assertFalse(descriptionText2.isDefaultNamespace("http://food"));
        assertFalse(descriptionText3.isDefaultNamespace("http://food"));
        assertTrue(option1.isDefaultNamespace("http://food"));
        assertTrue(option2.isDefaultNamespace("http://food"));
        assertTrue(option2Reference.isDefaultNamespace("http://food"));
        assertTrue(wafflemaker.isDefaultNamespace("http://food"));
        assertTrue(nutrition.isDefaultNamespace("http://food"));
        assertTrue(vitamins.isDefaultNamespace("http://food"));
        assertTrue(vitaminsXmlnsA.isDefaultNamespace("http://food"));
        assertTrue(comment.isDefaultNamespace("http://food"));
        assertTrue(vitaminc.isDefaultNamespace("http://food"));
        assertTrue(vitamincText.isDefaultNamespace("http://food"));
    }

    /**
     * Xerces fails this test. It returns false always for entity, notation,
     * document fragment and document type nodes. This contradicts its own
     * behaviour on lookupNamespaceURI(null).
     */
    public void testIsDefaultNamespaceNull_XercesBugs() {
        String message = "isDefaultNamespace() should be consistent with lookupNamespaceURI(null)";
        assertTrue(message, doctype.isDefaultNamespace(null));
        if (sp != null) {
            assertTrue(message, sp.isDefaultNamespace(null));
        }
        if (png != null) {
            assertTrue(message, png.isDefaultNamespace(null));
        }
    }

    public void testIsDefaultNamespaceNull() {
        assertTrue(document.isDefaultNamespace(null));
        assertTrue(menu.isDefaultNamespace(null));
        assertFalse(item.isDefaultNamespace(null));
        assertFalse(itemXmlns.isDefaultNamespace(null));
        assertFalse(itemXmlnsA.isDefaultNamespace(null));
        assertFalse(name.isDefaultNamespace(null));
        assertFalse(standard.isDefaultNamespace(null));
        assertFalse(deluxe.isDefaultNamespace(null));
        assertFalse(description.isDefaultNamespace(null));
        assertFalse(descriptionText1.isDefaultNamespace(null));
        assertFalse(descriptionText2.isDefaultNamespace(null));
        assertFalse(descriptionText3.isDefaultNamespace(null));
        assertFalse(option1.isDefaultNamespace(null));
        assertFalse(option2.isDefaultNamespace(null));
        assertFalse(option2Reference.isDefaultNamespace(null));
        assertFalse(wafflemaker.isDefaultNamespace(null));
        assertFalse(nutrition.isDefaultNamespace(null));
        assertFalse(vitamins.isDefaultNamespace(null));
        assertFalse(vitaminsXmlnsA.isDefaultNamespace(null));
        assertFalse(comment.isDefaultNamespace(null));
        assertFalse(vitaminc.isDefaultNamespace(null));
        assertFalse(vitamincText.isDefaultNamespace(null));
    }

    public void testDoctypeSetTextContent() throws TransformerException {
        String original = domToString(document);
        doctype.setTextContent("foobar"); // strangely, this is specified to no-op
        assertEquals(original, domToString(document));
    }

    public void testDocumentSetTextContent() throws TransformerException {
        String original = domToString(document);
        document.setTextContent("foobar"); // strangely, this is specified to no-op
        assertEquals(original, domToString(document));
    }

    public void testElementSetTextContent() throws TransformerException {
        String original = domToString(document);
        nutrition.setTextContent("foobar");
        String expected = original.replaceFirst(
                "(?s)<nutrition>.*</nutrition>", "<nutrition>foobar</nutrition>");
        assertEquals(expected, domToString(document));
    }

    public void testEntitySetTextContent() throws TransformerException {
        if (sp == null) {
            return;
        }
        try {
            sp.setTextContent("foobar");
            fail(); // is this implementation-specific behaviour?
        } catch (DOMException e) {
        }
    }

    public void testNotationSetTextContent() throws TransformerException {
        if (png == null) {
            return;
        }
        String original = domToString(document);
        png.setTextContent("foobar");
        String expected = original.replace("image/png", "foobar");
        assertEquals(expected, domToString(document));
    }

    /**
     * Tests setTextContent on entity references. Although the other tests can
     * act on a parsed DOM, this needs to use a programmatically constructed DOM
     * because the parser may have replaced the entity reference with the
     * corresponding text.
     */
    public void testEntityReferenceSetTextContent() throws TransformerException {
        document = builder.newDocument();
        Element root = document.createElement("menu");
        document.appendChild(root);

        EntityReference entityReference = document.createEntityReference("sp");
        root.appendChild(entityReference);

        try {
            entityReference.setTextContent("Lite Syrup");
            fail();
        } catch (DOMException e) {
        }
    }

    public void testAttributeSetTextContent() throws TransformerException {
        String original = domToString(document);
        standard.setTextContent("foobar");
        String expected = original.replace("standard=\"strawberry\"", "standard=\"foobar\"");
        assertEquals(expected, domToString(document));
    }

    public void testTextSetTextContent() throws TransformerException {
        String original = domToString(document);
        descriptionText1.setTextContent("foobar");
        String expected = original.replace(">Belgian<!", ">foobar<!");
        assertEquals(expected, domToString(document));
    }

    public void testCdataSetTextContent() throws TransformerException {
        String original = domToString(document);
        descriptionText2.setTextContent("foobar");
        String expected = original.replace(
                " waffles & strawberries (< 5g ", "foobar");
        assertEquals(expected, domToString(document));
    }

    public void testProcessingInstructionSetTextContent() throws TransformerException {
        String original = domToString(document);
        wafflemaker.setTextContent("foobar");
        String expected = original.replace(" square shape?>", " foobar?>");
        assertEquals(expected, domToString(document));
    }

    public void testCommentSetTextContent() throws TransformerException {
        String original = domToString(document);
        comment.setTextContent("foobar");
        String expected = original.replace("-- add other vitamins? --", "--foobar--");
        assertEquals(expected, domToString(document));
    }

    public void testCoreFeature() {
        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() {
        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");
    }

    /**
     * The RI fails this test.
     * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#Document3-version
     */
    public void testXmlVersionFeature() {
        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");
    }

    @KnownFailure("Dalvik doesn't support load/save")
    public void testLoadSaveFeature() {
        assertFeature("LS", "3.0");
    }

    @KnownFailure("Dalvik doesn't support the element traversal feature")
    public void testElementTraversalFeature() {
        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() {
        // we don't independently test the features; instead just assume the
        // implementation calls through to hasFeature (as tested above)
        for (Node node : allNodes) {
            assertTrue(node.isSupported("XML", null));
            assertTrue(node.isSupported("XML", "3.0"));
            assertFalse(node.isSupported("foo", null));
            assertFalse(node.isSupported("foo", "bar"));
        }
    }

    public void testGetFeature() {
        // we don't independently test the features; instead just assume the
        // implementation calls through to hasFeature (as tested above)
        for (Node node : allNodes) {
            assertSame(node, node.getFeature("XML", null));
            assertSame(node, node.getFeature("XML", "3.0"));
            assertNull(node.getFeature("foo", null));
            assertNull(node.getFeature("foo", "bar"));
        }
    }

    public void testNodeEqualsPositive() throws Exception {
        DomTest copy = new DomTest();
        copy.setUp();

        for (int i = 0; i < allNodes.size(); i++) {
            Node a = allNodes.get(i);
            Node b = copy.allNodes.get(i);
            assertTrue(a.isEqualNode(b));
        }
    }

    public void testNodeEqualsNegative() throws Exception {
        for (Node a : allNodes) {
            for (Node b : allNodes) {
                assertEquals(a == b, a.isEqualNode(b));
            }
        }
    }

    public void testNodeEqualsNegativeRecursive() throws Exception {
        DomTest copy = new DomTest();
        copy.setUp();
        copy.vitaminc.setTextContent("55%");

        // changing anything about a node should break equality for all parents
        assertFalse(document.isEqualNode(copy.document));
        assertFalse(menu.isEqualNode(copy.menu));
        assertFalse(item.isEqualNode(copy.item));
        assertFalse(nutrition.isEqualNode(copy.nutrition));
        assertFalse(vitamins.isEqualNode(copy.vitamins));
        assertFalse(vitaminc.isEqualNode(copy.vitaminc));

        // but not siblings
        assertTrue(doctype.isEqualNode(copy.doctype));
        assertTrue(description.isEqualNode(copy.description));
        assertTrue(option1.isEqualNode(copy.option1));
    }

    public void testNodeEqualsNull() {
        for (Node node : allNodes) {
            try {
                node.isEqualNode(null);
                fail();
            } catch (NullPointerException e) {
            }
        }
    }

    public void testIsElementContentWhitespaceWithoutDeclaration() throws Exception {
        String xml = "<menu>    <item/>   </menu>";

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        Text text = (Text) factory.newDocumentBuilder()
                .parse(new InputSource(new StringReader(xml)))
                .getDocumentElement().getChildNodes().item(0);
        assertFalse(text.isElementContentWhitespace());
    }

    @KnownFailure("Dalvik doesn't recognize element content whitespace")
    public void testIsElementContentWhitespaceWithDeclaration() throws Exception {
        String xml = "<!DOCTYPE menu [\n"
                + "  <!ELEMENT menu (item)*>\n"
                + "  <!ELEMENT item (#PCDATA)>\n"
                + "]><menu>    <item/>   </menu>";

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        Text text = (Text) factory.newDocumentBuilder()
                .parse(new InputSource(new StringReader(xml)))
                .getDocumentElement().getChildNodes().item(0);
        assertTrue("This implementation does not recognize element content whitespace",
                text.isElementContentWhitespace());
    }

    public void testGetWholeTextFirst() {
        assertEquals("Belgian waffles & strawberries (< 5g of fat)",
                descriptionText1.getWholeText());
    }

    public void testGetWholeTextMiddle() {
        assertEquals("This implementation doesn't include preceding nodes in getWholeText()",
                "Belgian waffles & strawberries (< 5g of fat)", descriptionText2.getWholeText());
    }

    public void testGetWholeTextLast() {
        assertEquals("This implementation doesn't include preceding nodes in getWholeText()",
                "Belgian waffles & strawberries (< 5g of fat)", descriptionText3.getWholeText());
    }

    public void testGetWholeTextOnly() {
        assertEquals("60%", vitamincText.getWholeText());
    }

    @KnownFailure("Dalvik doesn't resolve entity references")
    public void testGetWholeTextWithEntityReference() {
        EntityReference spReference = document.createEntityReference("sp");
        description.insertBefore(spReference, descriptionText2);

        assertEquals("This implementation doesn't resolve entity references in getWholeText()",
                "BelgianMaple Syrup waffles & strawberries (< 5g of fat)",
                descriptionText1.getWholeText());
    }

    public void testReplaceWholeTextFirst() throws TransformerException {
        String original = domToString(document);
        Text replacement = descriptionText1.replaceWholeText("Eggos");
        assertSame(descriptionText1, replacement);
        String expected = original.replace(
                "Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)", "Eggos");
        assertEquals(expected, domToString(document));
    }

    public void testReplaceWholeTextMiddle() throws TransformerException {
        String original = domToString(document);
        Text replacement = descriptionText2.replaceWholeText("Eggos");
        assertSame(descriptionText2, replacement);
        String expected = original.replace(
                "Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)", "<![CDATA[Eggos]]>");
        assertEquals("This implementation doesn't remove preceding nodes in replaceWholeText()",
                expected, domToString(document));
    }

    public void testReplaceWholeTextLast() throws TransformerException {
        String original = domToString(document);
        Text replacement = descriptionText3.replaceWholeText("Eggos");
        assertSame(descriptionText3, replacement);
        String expected = original.replace(
                "Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)", "Eggos");
        assertEquals("This implementation doesn't remove preceding nodes in replaceWholeText()",
                expected, domToString(document));
    }

    public void testReplaceWholeTextOnly() throws TransformerException {
        String original = domToString(document);
        Text replacement = vitamincText.replaceWholeText("70%");
        assertEquals(Node.TEXT_NODE, replacement.getNodeType());
        assertSame(vitamincText, replacement);
        String expected = original.replace("60%", "70%");
        assertEquals(expected, domToString(document));
    }

    public void testReplaceWholeTextFirstWithNull() throws TransformerException {
        String original = domToString(document);
        assertNull(descriptionText1.replaceWholeText(null));
        String expected = original.replaceFirst(">.*</description>", "/>");
        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
                expected, domToString(document));
    }

    public void testReplaceWholeTextMiddleWithNull() throws TransformerException {
        String original = domToString(document);
        assertNull(descriptionText2.replaceWholeText(null));
        String expected = original.replaceFirst(">.*</description>", "/>");
        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
                expected, domToString(document));
    }

    public void testReplaceWholeTextLastWithNull() throws TransformerException {
        String original = domToString(document);
        assertNull(descriptionText3.replaceWholeText(null));
        String expected = original.replaceFirst(">.*</description>", "/>");
        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
                expected, domToString(document));
    }

    public void testReplaceWholeTextFirstWithEmptyString() throws TransformerException {
        String original = domToString(document);
        assertNull(descriptionText1.replaceWholeText(""));
        String expected = original.replaceFirst(">.*</description>", "/>");
        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
                expected, domToString(document));
    }

    public void testReplaceWholeTextOnlyWithEmptyString() throws TransformerException {
        String original = domToString(document);
        assertNull(vitamincText.replaceWholeText(""));
        String expected = original.replaceFirst(">.*</a:vitaminc>", "/>");
        assertEquals(expected, domToString(document));
    }

    public void testUserDataAttachments() {
        Object a = new Object();
        Object b = new Object();
        for (Node node : allNodes) {
            node.setUserData("a", a, null);
            node.setUserData("b", b, null);
        }
        for (Node node : allNodes) {
            assertSame(a, node.getUserData("a"));
            assertSame(b, node.getUserData("b"));
            assertEquals(null, node.getUserData("c"));
            assertEquals(null, node.getUserData("A"));
        }
    }

    public void testUserDataRejectsNullKey() {
        try {
            menu.setUserData(null, "apple", null);
            fail();
        } catch (NullPointerException e) {
        }
        try {
            menu.getUserData(null);
            fail();
        } catch (NullPointerException e) {
        }
    }

    /**
     * A shallow clone requires cloning the attributes but not the child nodes.
     */
    public void testUserDataHandlerNotifiedOfShallowClones() {
        RecordingHandler handler = new RecordingHandler();
        name.setUserData("a", "apple", handler);
        name.setUserData("b", "banana", handler);
        standard.setUserData("c", "cat", handler);
        waffles.setUserData("d", "dog", handler);

        Element clonedName = (Element) name.cloneNode(false);
        Attr clonedStandard = clonedName.getAttributeNode("a:standard");

        Set<String> expected = new HashSet<String>();
        expected.add(notification(NODE_CLONED, "a", "apple", name, clonedName));
        expected.add(notification(NODE_CLONED, "b", "banana", name, clonedName));
        expected.add(notification(NODE_CLONED, "c", "cat", standard, clonedStandard));
        assertEquals(expected, handler.calls);
    }

    /**
     * A deep clone requires cloning both the attributes and the child nodes.
     */
    public void testUserDataHandlerNotifiedOfDeepClones() {
        RecordingHandler handler = new RecordingHandler();
        name.setUserData("a", "apple", handler);
        name.setUserData("b", "banana", handler);
        standard.setUserData("c", "cat", handler);
        waffles.setUserData("d", "dog", handler);

        Element clonedName = (Element) name.cloneNode(true);
        Attr clonedStandard = clonedName.getAttributeNode("a:standard");
        Text clonedWaffles = (Text) clonedName.getChildNodes().item(0);

        Set<String> expected = new HashSet<String>();
        expected.add(notification(NODE_CLONED, "a", "apple", name, clonedName));
        expected.add(notification(NODE_CLONED, "b", "banana", name, clonedName));
        expected.add(notification(NODE_CLONED, "c", "cat", standard, clonedStandard));
        expected.add(notification(NODE_CLONED, "d", "dog", waffles, clonedWaffles));
        assertEquals(expected, handler.calls);
    }

    /**
     * A shallow import requires importing the attributes but not the child
     * nodes.
     */
    public void testUserDataHandlerNotifiedOfShallowImports() {
        RecordingHandler handler = new RecordingHandler();
        name.setUserData("a", "apple", handler);
        name.setUserData("b", "banana", handler);
        standard.setUserData("c", "cat", handler);
        waffles.setUserData("d", "dog", handler);

        Document newDocument = builder.newDocument();
        Element importedName = (Element) newDocument.importNode(name, false);
        Attr importedStandard = importedName.getAttributeNode("a:standard");

        Set<String> expected = new HashSet<String>();
        expected.add(notification(NODE_IMPORTED, "a", "apple", name, importedName));
        expected.add(notification(NODE_IMPORTED, "b", "banana", name, importedName));
        expected.add(notification(NODE_IMPORTED, "c", "cat", standard, importedStandard));
        assertEquals(expected, handler.calls);
    }

    /**
     * A deep import requires cloning both the attributes and the child nodes.
     */
    public void testUserDataHandlerNotifiedOfDeepImports() {
        RecordingHandler handler = new RecordingHandler();
        name.setUserData("a", "apple", handler);
        name.setUserData("b", "banana", handler);
        standard.setUserData("c", "cat", handler);
        waffles.setUserData("d", "dog", handler);

        Document newDocument = builder.newDocument();
        Element importedName = (Element) newDocument.importNode(name, true);
        Attr importedStandard = importedName.getAttributeNode("a:standard");
        Text importedWaffles = (Text) importedName.getChildNodes().item(0);

        Set<String> expected = new HashSet<String>();
        expected.add(notification(NODE_IMPORTED, "a", "apple", name, importedName));
        expected.add(notification(NODE_IMPORTED, "b", "banana", name, importedName));
        expected.add(notification(NODE_IMPORTED, "c", "cat", standard, importedStandard));
        expected.add(notification(NODE_IMPORTED, "d", "dog", waffles, importedWaffles));
        assertEquals(expected, handler.calls);
    }

    public void testImportNodeDeep() throws TransformerException {
        String original = domToStringStripElementWhitespace(document);

        Document newDocument = builder.newDocument();
        Element importedItem = (Element) newDocument.importNode(item, true);
        assertDetached(item.getParentNode(), importedItem);

        newDocument.appendChild(importedItem);
        String expected = original.replaceAll("</?menu>", "");
        assertEquals(expected, domToStringStripElementWhitespace(newDocument));
    }

    public void testImportNodeShallow() throws TransformerException {
        Document newDocument = builder.newDocument();
        Element importedItem = (Element) newDocument.importNode(item, false);
        assertDetached(item.getParentNode(), importedItem);

        newDocument.appendChild(importedItem);
        assertEquals("<item xmlns=\"http://food\" xmlns:a=\"http://addons\"/>",
                domToString(newDocument));
    }

    public void testNodeAdoption() throws Exception {
        for (Node node : allNodes) {
            if (node == document || node == doctype || node == sp || node == png) {
                assertNotAdoptable(node);
            } else {
                adoptAndCheck(node);
            }
        }
    }

    private void assertNotAdoptable(Node node) {
        try {
            builder.newDocument().adoptNode(node);
            fail();
        } catch (DOMException e) {
        }
    }

    /**
     * Adopts the node into another document, then adopts the root element, and
     * then attaches the adopted node in the proper place. The net result should
     * be that the document's entire contents have moved to another document.
     */
    private void adoptAndCheck(Node node) throws Exception {
        String original = domToString(document);
        Document newDocument = builder.newDocument();

        // remember where to insert the node in the new document
        boolean isAttribute = node.getNodeType() == Node.ATTRIBUTE_NODE;
        Node parent = isAttribute
                ? ((Attr) node).getOwnerElement() : node.getParentNode();
        Node nextSibling = node.getNextSibling();

        // move the node and make sure it was detached
        assertSame(node, newDocument.adoptNode(node));
        assertDetached(parent, node);

        // move the rest of the document and wire the adopted back into place
        assertSame(menu, newDocument.adoptNode(menu));
        newDocument.appendChild(menu);
        if (isAttribute) {
            ((Element) parent).setAttributeNodeNS((Attr) node);
        } else if (nextSibling != null) {
            parent.insertBefore(node, nextSibling);
        } else if (parent != document) {
            parent.appendChild(node);
        }

        assertEquals(original, domToString(newDocument));
        document = newDocument;
    }

    private void assertDetached(Node formerParent, Node node) {
        assertNull(node.getParentNode());
        NodeList children = formerParent.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            assertTrue(children.item(i) != node);
        }
        if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
            assertNull(((Attr) node).getOwnerElement());
            NamedNodeMap attributes = formerParent.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                assertTrue(attributes.item(i) != node);
            }
        }
    }

    public void testAdoptionImmediatelyAfterParsing() throws Exception {
        Document newDocument = builder.newDocument();
        try {
            assertSame(name, newDocument.adoptNode(name));
            assertSame(newDocument, name.getOwnerDocument());
            assertSame(newDocument, standard.getOwnerDocument());
            assertSame(newDocument, waffles.getOwnerDocument());
        } catch (Throwable e) {
            AssertionFailedError failure = new AssertionFailedError(
                    "This implementation fails to adopt nodes before the "
                            + "document has been traversed");
            failure.initCause(e);
            throw failure;
        }
    }

    /**
     * There should be notifications for adopted node itself but none of its
     * children. The DOM spec is vague on this, so we're consistent with the RI.
     */
    public void testUserDataHandlerNotifiedOfOnlyShallowAdoptions() throws Exception {
        /*
         * Force a traversal of the document, otherwise this test may fail for
         * an unrelated reason on version 5 of the RI. That behavior is
         * exercised by testAdoptionImmediatelyAfterParsing().
         */
        domToString(document);

        RecordingHandler handler = new RecordingHandler();
        name.setUserData("a", "apple", handler);
        name.setUserData("b", "banana", handler);
        standard.setUserData("c", "cat", handler);
        waffles.setUserData("d", "dog", handler);

        Document newDocument = builder.newDocument();
        assertSame(name, newDocument.adoptNode(name));
        assertSame(newDocument, name.getOwnerDocument());
        assertSame(newDocument, standard.getOwnerDocument());
        assertSame(newDocument, waffles.getOwnerDocument());

        Set<String> expected = new HashSet<String>();
        expected.add(notification(NODE_ADOPTED, "a", "apple", name, null));
        expected.add(notification(NODE_ADOPTED, "b", "banana", name, null));
        assertEquals(expected, handler.calls);
    }

    public void testBaseUriRelativeUriResolution() throws Exception {
        File file = File.createTempFile("DomTest.java", "xml");
        File parentFile = file.getParentFile();
        FileWriter writer = new FileWriter(file);
        writer.write("<a>"
                + "  <b xml:base=\"b1/b2\">"
                + "    <c>"
                + "      <d xml:base=\"../d1/d2\"><e/></d>"
                + "    </c>"
                + "  </b>"
                + "  <h xml:base=\"h1/h2/\">"
                + "    <i xml:base=\"../i1/i2\"/>"
                + "  </h>"
                + "</a>");
        writer.close();
        document = builder.parse(file);

        assertFileUriEquals("", file.getPath(), document.getBaseURI());
        assertFileUriEquals("", file.getPath(), document.getDocumentURI());
        Element a = document.getDocumentElement();
        assertFileUriEquals("", file.getPath(), a.getBaseURI());

        String message = "This implementation's getBaseURI() doesn't handle relative URIs";
        Element b = (Element) a.getChildNodes().item(1);
        Element c = (Element) b.getChildNodes().item(1);
        Element d = (Element) c.getChildNodes().item(1);
        Element e = (Element) d.getChildNodes().item(0);
        Element h = (Element) a.getChildNodes().item(3);
        Element i = (Element) h.getChildNodes().item(1);
        assertFileUriEquals(message, parentFile + "/b1/b2", b.getBaseURI());
        assertFileUriEquals(message, parentFile + "/b1/b2", c.getBaseURI());
        assertFileUriEquals(message, parentFile + "/d1/d2", d.getBaseURI());
        assertFileUriEquals(message, parentFile + "/d1/d2", e.getBaseURI());
        assertFileUriEquals(message, parentFile + "/h1/h2/", h.getBaseURI());
        assertFileUriEquals(message, parentFile + "/h1/i1/i2", i.getBaseURI());
    }

    /**
     * Regrettably both "file:/tmp/foo.txt" and "file:///tmp/foo.txt" are
     * legal URIs, and different implementations emit different forms.
     */
    private void assertFileUriEquals(
            String message, String expectedFile, String actual) {
        if (!("file:" + expectedFile).equals(actual)
                && !("file://" + expectedFile).equals(actual)) {
            fail("Expected URI for: " + expectedFile
                    + " but was " + actual + ". " + message);
        }
    }

    /**
     * According to the <a href="http://www.w3.org/TR/xmlbase/">XML Base</a>
     * spec, fragments (like "#frag" or "") should not be dereferenced.
     */
    public void testBaseUriResolutionWithHashes() throws Exception {
        document = builder.parse(new InputSource(new StringReader(
                "<a xml:base=\"http://a1/a2\">"
                        + "  <b xml:base=\"b1#b2\"/>"
                        + "  <c xml:base=\"#c1\">"
                        + "    <d xml:base=\"\"/>"
                        + "  </c>"
                        + "  <e xml:base=\"\"/>"
                        + "</a>")));
        Element a = document.getDocumentElement();
        assertEquals("http://a1/a2", a.getBaseURI());

        String message = "This implementation's getBaseURI() doesn't handle "
                + "relative URIs with hashes";
        Element b = (Element) a.getChildNodes().item(1);
        Element c = (Element) a.getChildNodes().item(3);
        Element d = (Element) c.getChildNodes().item(1);
        Element e = (Element) a.getChildNodes().item(5);
        assertEquals(message, "http://a1/b1#b2", b.getBaseURI());
        assertEquals(message, "http://a1/a2#c1", c.getBaseURI());
        assertEquals(message, "http://a1/a2#c1", d.getBaseURI());
        assertEquals(message, "http://a1/a2", e.getBaseURI());
    }

    public void testBaseUriInheritedForProcessingInstructions() {
        document.setDocumentURI("http://d1/d2");
        assertEquals("http://d1/d2", wafflemaker.getBaseURI());
    }

    public void testBaseUriInheritedForEntities() {
        if (sp == null) {
            return;
        }
        document.setDocumentURI("http://d1/d2");
        assertEquals("http://d1/d2", sp.getBaseURI());
    }

    public void testBaseUriNotInheritedForNotations() {
        if (png == null) {
            return;
        }
        document.setDocumentURI("http://d1/d2");
        assertNull(png.getBaseURI());
    }

    public void testBaseUriNotInheritedForDoctypes() {
        document.setDocumentURI("http://d1/d2");
        assertNull(doctype.getBaseURI());
    }

    public void testBaseUriNotInheritedForAttributes() {
        document.setDocumentURI("http://d1/d2");
        assertNull(itemXmlns.getBaseURI());
        assertNull(itemXmlnsA.getBaseURI());
        assertNull(standard.getBaseURI());
        assertNull(vitaminsXmlnsA.getBaseURI());
    }

    public void testBaseUriNotInheritedForTextsOrCdatas() {
        document.setDocumentURI("http://d1/d2");
        assertNull(descriptionText1.getBaseURI());
        assertNull(descriptionText2.getBaseURI());
        assertNull(option2Reference.getBaseURI());
    }

    public void testBaseUriNotInheritedForComments() {
        document.setDocumentURI("http://d1/d2");
        assertNull(descriptionText1.getBaseURI());
        assertNull(descriptionText2.getBaseURI());
    }

    public void testBaseUriNotInheritedForEntityReferences() {
        document.setDocumentURI("http://d1/d2");
        assertNull(option2Reference.getBaseURI());
    }

    public void testProgrammaticElementIds() {
        vitaminc.setAttribute("name", "c");
        assertFalse(vitaminc.getAttributeNode("name").isId());
        assertNull(document.getElementById("c"));

        // set the ID attribute...
        vitaminc.setIdAttribute("name", true);
        assertTrue(vitaminc.getAttributeNode("name").isId());
        assertSame(vitaminc, document.getElementById("c"));

        // ... and then take it away
        vitaminc.setIdAttribute("name", false);
        assertFalse(vitaminc.getAttributeNode("name").isId());
        assertNull(document.getElementById("c"));
    }

    public void testMultipleIdsOnOneElement() {
        vitaminc.setAttribute("name", "c");
        vitaminc.setIdAttribute("name", true);
        vitaminc.setAttribute("atc", "a11g");
        vitaminc.setIdAttribute("atc", true);

        assertTrue(vitaminc.getAttributeNode("name").isId());
        assertTrue(vitaminc.getAttributeNode("atc").isId());
        assertSame(vitaminc, document.getElementById("c"));
        assertSame(vitaminc, document.getElementById("a11g"));
        assertNull(document.getElementById("g"));
    }

    @KnownFailure("Dalvik treats id attributes as identifiers")
    public void testAttributeNamedIdIsNotAnIdByDefault() {
        String message = "This implementation incorrectly interprets the "
                + "\"id\" attribute as an identifier by default.";
        vitaminc.setAttribute("id", "c");
        assertNull(message, document.getElementById("c"));
    }

    public void testElementTypeInfo() {
        TypeInfo typeInfo = description.getSchemaTypeInfo();
        assertNull(typeInfo.getTypeName());
        assertNull(typeInfo.getTypeNamespace());
        assertFalse(typeInfo.isDerivedFrom("x", "y", TypeInfo.DERIVATION_UNION));
    }

    public void testAttributeTypeInfo() {
        TypeInfo typeInfo = standard.getSchemaTypeInfo();
        assertNull(typeInfo.getTypeName());
        assertNull(typeInfo.getTypeNamespace());
        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) {
            }
        }
    }

    public void testDocumentDoesNotHaveWhitespaceChildren()
            throws IOException, SAXException {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>  \n"
                + "   <foo/>\n"
                + "  \n";
        document = builder.parse(new InputSource(new StringReader(xml)));
        assertEquals("Document nodes shouldn't have text children",
                1, document.getChildNodes().getLength());
    }

    @KnownFailure("Dalvik document nodes accept arbitrary child nodes")
    public void testDocumentAddChild()
            throws IOException, SAXException {
        try {
            document.appendChild(document.createTextNode("   "));
            fail("Document nodes shouldn't accept child nodes");
        } catch (DOMException e) {
        }
    }

    public void testIterateForwardsThroughInnerNodeSiblings() throws Exception {
        document = builder.parse(new InputSource(new StringReader(
                "<root><child/><child/></root>")));
        Node root = document.getDocumentElement();
        Node current = root.getChildNodes().item(0);
        while (current.getNextSibling() != null) {
            current = current.getNextSibling();
        }
        assertEquals(root.getChildNodes().item(root.getChildNodes().getLength() - 1), current);
    }

    public void testIterateBackwardsThroughInnerNodeSiblings() throws Exception {
        document = builder.parse(new InputSource(new StringReader(
                "<root><child/><child/></root>")));
        Node root = document.getDocumentElement();
        Node current = root.getChildNodes().item(root.getChildNodes().getLength() - 1);
        while (current.getPreviousSibling() != null) {
            current = current.getPreviousSibling();
        }
        assertEquals(root.getChildNodes().item(0), current);
    }

    public void testIterateForwardsThroughLeafNodeSiblings() throws Exception {
        document = builder.parse(new InputSource(new StringReader(
                "<root> <!-- --> </root>")));
        Node root = document.getDocumentElement();
        Node current = root.getChildNodes().item(0);
        while (current.getNextSibling() != null) {
            current = current.getNextSibling();
        }
        assertEquals(root.getChildNodes().item(root.getChildNodes().getLength() - 1), current);
    }

    public void testIterateBackwardsThroughLeafNodeSiblings() throws Exception {
        document = builder.parse(new InputSource(new StringReader(
                "<root> <!-- --> </root>")));
        Node root = document.getDocumentElement();
        Node current = root.getChildNodes().item(root.getChildNodes().getLength() - 1);
        while (current.getPreviousSibling() != null) {
            current = current.getPreviousSibling();
        }
        assertEquals(root.getChildNodes().item(0), current);
    }

    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) {
            calls.add(notification(operation, key, data, src, dst));
        }
    }

    private String notification(short operation, String key, Object data, Node src, Node dst) {
        return "op:" + operation + " key:" + key + " data:" + data + " src:" + src + " dst:" + dst;
    }

    private String domToString(Document document) throws TransformerException {
        StringWriter writer = new StringWriter();
        transformer.transform(new DOMSource(document), new StreamResult(writer));
        String result = writer.toString();

        /*
         * Hack: swap <name>'s a:standard attribute and deluxe attribute if
         * they're out of order. Some document transformations reorder the
         * attributes, which causes pain when we try to use String comparison on
         * them.
         */
        Matcher attributeMatcher = Pattern.compile(" a:standard=\"[^\"]+\"").matcher(result);
        if (attributeMatcher.find()) {
            result = result.substring(0, attributeMatcher.start())
                    + result.substring(attributeMatcher.end());
            int insertionPoint = result.indexOf(" deluxe=\"");
            result = result.substring(0, insertionPoint)
                    + attributeMatcher.group()
                    + result.substring(insertionPoint);
        }

        return result;
    }

    private String domToStringStripElementWhitespace(Document document)
            throws TransformerException {
        return domToString(document).replaceAll("(?m)>\\s+<", "><");
    }
}
