Merge "New implementation for DOMConfiguration."
diff --git a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DOMConfigurationImpl.java b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DOMConfigurationImpl.java
new file mode 100644
index 0000000..2f57a4c
--- /dev/null
+++ b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DOMConfigurationImpl.java
@@ -0,0 +1,371 @@
+/*
+ * 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 org.apache.harmony.xml.dom;
+
+import org.w3c.dom.DOMConfiguration;
+import org.w3c.dom.DOMErrorHandler;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.DOMStringList;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A minimal implementation of DOMConfiguration. This implementation uses inner
+ * parameter instances to centralize each parameter's behaviour.
+ */
+public final class DOMConfigurationImpl implements DOMConfiguration {
+
+    private static final Map<String, Parameter> PARAMETERS
+            = new TreeMap<String, Parameter>(String.CASE_INSENSITIVE_ORDER);
+
+    static {
+        /*
+         * True to canonicalize the document (unsupported). This includes
+         * removing DocumentType nodes from the tree and removing unused
+         * namespace declarations. Setting this to true also sets these
+         * parameters:
+         *   entities = false
+         *   normalize-characters = false
+         *   cdata-sections = false
+         *   namespaces = true
+         *   namespace-declarations = true
+         *   well-formed = true
+         *   element-content-whitespace = true
+         * Setting these parameters to another value shall revert the canonical
+         * form to false.
+         */
+        PARAMETERS.put("canonical-form", new FixedParameter(false));
+
+        /*
+         * True to keep existing CDATA nodes; false to replace them/merge them
+         * into adjacent text nodes.
+         */
+        PARAMETERS.put("cdata-sections", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.cdataSections;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.cdataSections = (Boolean) value;
+            }
+        });
+
+        /*
+         * True to check character normalization (unsupported).
+         */
+        PARAMETERS.put("check-character-normalization", new FixedParameter(false));
+
+        /*
+         * True to keep comments in the document; false to discard them.
+         */
+        PARAMETERS.put("comments", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.comments;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.comments = (Boolean) value;
+            }
+        });
+
+        /*
+         * True to expose schema normalized values. Setting this to true sets
+         * the validate parameter to true. Has no effect when validate is false.
+         */
+        PARAMETERS.put("datatype-normalization", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.datatypeNormalization;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                if ((Boolean) value) {
+                    config.datatypeNormalization = true;
+                    config.validate = true;
+                } else {
+                    config.datatypeNormalization = false;
+                }
+            }
+        });
+
+        /*
+         * True to keep whitespace elements in the document; false to discard
+         * them (unsupported).
+         */
+        PARAMETERS.put("element-content-whitespace", new FixedParameter(true));
+
+        /*
+         * True to keep entity references in the document; false to expand them.
+         */
+        PARAMETERS.put("entities", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.entities;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.entities = (Boolean) value;
+            }
+        });
+
+        /*
+         * Handler to be invoked when errors are encountered.
+         */
+        PARAMETERS.put("error-handler", new Parameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.errorHandler;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.errorHandler = (DOMErrorHandler) value;
+            }
+            public boolean canSet(DOMConfigurationImpl config, Object value) {
+                return value == null || value instanceof DOMErrorHandler;
+            }
+        });
+
+        /*
+         * Bulk alias to set the following parameter values:
+         *   validate-if-schema = false
+         *   entities = false
+         *   datatype-normalization = false
+         *   cdata-sections = false
+         *   namespace-declarations = true
+         *   well-formed = true
+         *   element-content-whitespace = true
+         *   comments = true
+         *   namespaces = true.
+         * Querying this returns true if all of the above parameters have the
+         * listed values; false otherwise.
+         */
+        PARAMETERS.put("infoset", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                // validate-if-schema is always false
+                // element-content-whitespace is always true
+                // namespace-declarations is always true
+                return !config.entities
+                        && !config.datatypeNormalization
+                        && !config.cdataSections
+                        && config.wellFormed
+                        && config.comments
+                        && config.namespaces;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                if ((Boolean) value) {
+                    // validate-if-schema is always false
+                    // element-content-whitespace is always true
+                    // namespace-declarations is always true
+                    config.entities = false;
+                    config.datatypeNormalization = false;
+                    config.cdataSections = false;
+                    config.wellFormed = true;
+                    config.comments = true;
+                    config.namespaces = true;
+                }
+            }
+        });
+
+        /*
+         * True to perform namespace processing; false for none.
+         */
+        PARAMETERS.put("namespaces", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.namespaces;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.namespaces = (Boolean) value;
+            }
+        });
+
+        /**
+         * True to include namespace declarations; false to discard them
+         * (unsupported). Even when namespace declarations are discarded,
+         * prefixes are retained.
+         *
+         * Has no effect if namespaces is false.
+         */
+        PARAMETERS.put("namespace-declarations", new FixedParameter(true));
+
+        /*
+         * True to fully normalize characters (unsupported).
+         */
+        PARAMETERS.put("normalize-characters", new FixedParameter(false));
+
+        /*
+         * A list of whitespace-separated URIs representing the schemas to validate
+         * against. Has no effect if schema-type is null.
+         */
+        PARAMETERS.put("schema-location", new Parameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.schemaLocation;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.schemaLocation = (String) value;
+            }
+            public boolean canSet(DOMConfigurationImpl config, Object value) {
+                return value == null || value instanceof String;
+            }
+        });
+
+        /*
+         * URI representing the type of schema language, such as
+         * "http://www.w3.org/2001/XMLSchema" or "http://www.w3.org/TR/REC-xml".
+         */
+        PARAMETERS.put("schema-type", new Parameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.schemaType;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.schemaType = (String) value;
+            }
+            public boolean canSet(DOMConfigurationImpl config, Object value) {
+                return value == null || value instanceof String;
+            }
+        });
+
+        /*
+         * True to split CDATA sections containing "]]>"; false to signal an
+         * error instead.
+         */
+        PARAMETERS.put("split-cdata-sections", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.splitCdataSections;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.splitCdataSections = (Boolean) value;
+            }
+        });
+
+        /*
+         * True to require validation against a schema or DTD. Validation will
+         * recompute element content whitespace, ID and schema type data.
+         *
+         * Setting this unsets validate-if-schema.
+         */
+        PARAMETERS.put("validate", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.validate;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                // validate-if-schema is always false
+                config.validate = (Boolean) value;
+            }
+        });
+
+        /*
+         * True to validate if a schema was declared (unsupported). Setting this
+         * unsets validate.
+         */
+        PARAMETERS.put("validate-if-schema", new FixedParameter(false));
+
+        /*
+         * True to report invalid characters in node names, attributes, elements,
+         * comments, text, CDATA sections and processing instructions.
+         */
+        PARAMETERS.put("well-formed", new BooleanParameter() {
+            public Object get(DOMConfigurationImpl config) {
+                return config.wellFormed;
+            }
+            public void set(DOMConfigurationImpl config, Object value) {
+                config.wellFormed = (Boolean) value;
+            }
+        });
+
+        // TODO add "resource-resolver" property for use with LS feature...
+    }
+
+    private boolean cdataSections = true;
+    private boolean comments = true;
+    private boolean datatypeNormalization = false;
+    private boolean entities = true;
+    private DOMErrorHandler errorHandler;
+    private boolean namespaces = true;
+    private String schemaLocation;
+    private String schemaType;
+    private boolean splitCdataSections = true;
+    private boolean validate = false;
+    private boolean wellFormed = true;
+
+    interface Parameter {
+        Object get(DOMConfigurationImpl config);
+        void set(DOMConfigurationImpl config, Object value);
+        boolean canSet(DOMConfigurationImpl config, Object value);
+    }
+
+    static class FixedParameter implements Parameter {
+        final Object onlyValue;
+        FixedParameter(Object onlyValue) {
+            this.onlyValue = onlyValue;
+        }
+        public Object get(DOMConfigurationImpl config) {
+            return onlyValue;
+        }
+        public void set(DOMConfigurationImpl config, Object value) {
+            if (!onlyValue.equals(value)) {
+                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
+                        "Unsupported value: " + value);
+            }
+        }
+        public boolean canSet(DOMConfigurationImpl config, Object value) {
+            return onlyValue.equals(value);
+        }
+    }
+
+    static abstract class BooleanParameter implements Parameter {
+        public boolean canSet(DOMConfigurationImpl config, Object value) {
+            return value instanceof Boolean;
+        }
+    }
+
+    public boolean canSetParameter(String name, Object value) {
+        Parameter parameter = PARAMETERS.get(name);
+        return parameter != null && parameter.canSet(this, value);
+    }
+
+    public void setParameter(String name, Object value) throws DOMException {
+        Parameter parameter = PARAMETERS.get(name);
+        if (parameter == null) {
+            throw new DOMException(DOMException.NOT_FOUND_ERR, "No such parameter: " + name);
+        }
+        try {
+            parameter.set(this, value);
+        } catch (NullPointerException e) {
+            throw new DOMException(DOMException.TYPE_MISMATCH_ERR,
+                    "Null not allowed for " + name);
+        } catch (ClassCastException e) {
+            throw new DOMException(DOMException.TYPE_MISMATCH_ERR,
+                    "Invalid type for " + name + ": " + value.getClass());
+        }
+    }
+
+    public Object getParameter(String name) throws DOMException {
+        Parameter parameter = PARAMETERS.get(name);
+        if (parameter == null) {
+            throw new DOMException(DOMException.NOT_FOUND_ERR, "No such parameter: " + name);
+        }
+        return parameter.get(this);
+    }
+
+    public DOMStringList getParameterNames() {
+        final String[] result = PARAMETERS.keySet().toArray(new String[PARAMETERS.size()]);
+        return new DOMStringList() {
+            public String item(int index) {
+                return index < result.length ? result[index] : null;
+            }
+            public int getLength() {
+                return result.length;
+            }
+            public boolean contains(String str) {
+                return PARAMETERS.containsKey(str); // case-insensitive.
+            }
+        };
+    }
+}
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 e297280..035e1bb 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
@@ -47,6 +47,7 @@
 public class DocumentImpl extends InnerNodeImpl implements Document {
 
     private DOMImplementation domImplementation;
+    private DOMConfiguration domConfiguration;
 
     /*
      * The default values of these fields are specified by the Document
@@ -361,7 +362,10 @@
     }
 
     public DOMConfiguration getDomConfig() {
-        throw new UnsupportedOperationException(); // TODO
+        if (domConfiguration == null) {
+            domConfiguration = new DOMConfigurationImpl();
+        }
+        return domConfiguration;
     }
 
     public void normalizeDocument() {
diff --git a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java
index 275bbf3..9cee352 100644
--- a/libcore/xml/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java
+++ b/libcore/xml/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java
@@ -148,28 +148,35 @@
         return false;
     }
 
+    /**
+     * Normalize the text nodes within this subtree. Although named similarly,
+     * this method is unrelated to Document.normalize.
+     */
     @Override
-    public void normalize() {
-        Node nextNode = null;
-        
+    public final void normalize() {
+        Text next = null; // null if next doesn't exist or is not a TEXT_NODE
         for (int i = children.size() - 1; i >= 0; i--) {
-            Node thisNode = children.get(i);
+            Node node = children.get(i);
+            node.normalize();
 
-            thisNode.normalize();
-            
-            if (thisNode.getNodeType() == Node.TEXT_NODE) {
-                if (nextNode != null && nextNode.getNodeType() == Node.TEXT_NODE) {
-                    ((Text)thisNode).setData(thisNode.getNodeValue() + nextNode.getNodeValue());
-                    removeChild(nextNode);
-                }
-                
-                if ("".equals(thisNode.getNodeValue())) {
-                    removeChild(thisNode);
-                    nextNode = null;
-                } else {
-                    nextNode = thisNode;
-                }
+            if (node.getNodeType() != Node.TEXT_NODE) {
+                next = null;
+                continue;
             }
+
+            Text text = (Text) node;
+
+            if (text.getLength() == 0) {
+                removeChild(text);
+                continue;
+            }
+
+            if (next != null) {
+                text.appendData(next.getData());
+                removeChild(next);
+            }
+
+            next = text;
         }
     }
 
diff --git a/libcore/xml/src/test/java/tests/xml/AllTests.java b/libcore/xml/src/test/java/tests/xml/AllTests.java
index beabd08..96b96c5 100644
--- a/libcore/xml/src/test/java/tests/xml/AllTests.java
+++ b/libcore/xml/src/test/java/tests/xml/AllTests.java
@@ -29,7 +29,8 @@
         suite.addTestSuite(SimpleParserTest.class);
         suite.addTestSuite(SimpleBuilderTest.class);
         suite.addTestSuite(NodeTest.class);
-        
+        suite.addTestSuite(NormalizeTest.class);
+
         //suite.addTest(tests.org.w3c.dom.AllTests.suite());
         suite.addTest(tests.api.javax.xml.parsers.AllTests.suite());
 
diff --git a/libcore/xml/src/test/java/tests/xml/NormalizeTest.java b/libcore/xml/src/test/java/tests/xml/NormalizeTest.java
new file mode 100644
index 0000000..b10ea9c
--- /dev/null
+++ b/libcore/xml/src/test/java/tests/xml/NormalizeTest.java
@@ -0,0 +1,404 @@
+/*
+ * 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.DOMConfiguration;
+import org.w3c.dom.DOMError;
+import org.w3c.dom.DOMErrorHandler;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+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.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests the acceptance of various parameters on the DOM configuration. This
+ * test assumes the same set of parameters as the RI version 1.5. Perfectly
+ * correct DOM implementations may fail this test because it assumes certain
+ * parameters will be unsupported.
+ */
+public class NormalizeTest extends TestCase {
+
+    private Document document;
+    private DOMConfiguration domConfiguration;
+
+    String[] infosetImpliesFalse = {
+            "validate-if-schema", "entities", "datatype-normalization", "cdata-sections" };
+    String[] infosetImpliesTrue = { "namespace-declarations", "well-formed",
+            "element-content-whitespace", "comments", "namespaces" };
+
+    @Override protected void setUp() throws Exception {
+        document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+        domConfiguration = document.getDomConfig();
+    }
+
+    public void testCanonicalForm() {
+        assertSupported("canonical-form", false);
+        assertUnsupported("canonical-form", true);
+    }
+
+    public void testCdataSections() {
+        assertSupported("cdata-sections", false);
+        assertSupported("cdata-sections", true);
+    }
+
+    public void testCheckCharacterNormalization() {
+        assertSupported("check-character-normalization", false);
+        assertUnsupported("check-character-normalization", true);
+    }
+
+    public void testComments() {
+        assertSupported("comments", false);
+        assertSupported("comments", true);
+    }
+
+    public void testDatatypeNormalization() {
+        assertSupported("datatype-normalization", false);
+        assertSupported("datatype-normalization", true);
+
+        // setting this parameter to true should set validate to true...
+        domConfiguration.setParameter("validate", false);
+        domConfiguration.setParameter("datatype-normalization", true);
+        assertEquals(true, domConfiguration.getParameter("validate"));
+
+        // ...but the negative case isn't so
+        domConfiguration.setParameter("datatype-normalization", false);
+        assertEquals(true, domConfiguration.getParameter("validate"));
+    }
+
+    public void testElementContentWhitespace() {
+        assertUnsupported("element-content-whitespace", false);
+        assertSupported("element-content-whitespace", true);
+    }
+
+    public void testEntities() {
+        assertSupported("entities", false);
+        assertSupported("entities", true);
+    }
+
+    public void testErrorHandler() {
+        assertSupported("error-handler", null);
+        assertSupported("error-handler", new DOMErrorHandler() {
+            public boolean handleError(DOMError error) {
+                return true;
+            }
+        });
+    }
+
+    public void testInfoset() {
+        assertSupported("infoset", false);
+        assertSupported("infoset", true);
+    }
+
+    public void testSettingInfosetUpdatesImplied() {
+        // first clear those other parameters
+        for (String name : infosetImpliesFalse) {
+            if (domConfiguration.canSetParameter(name, true)) {
+                domConfiguration.setParameter(name, true);
+            }
+        }
+        for (String name : infosetImpliesTrue) {
+            if (domConfiguration.canSetParameter(name, false)) {
+                domConfiguration.setParameter(name, false);
+            }
+        }
+
+        // set infoset
+        domConfiguration.setParameter("infoset", true);
+
+        // now the parameters should all match what infoset implies
+        for (String name : infosetImpliesFalse) {
+            assertEquals(false, domConfiguration.getParameter(name));
+        }
+        for (String name : infosetImpliesTrue) {
+            assertEquals(true, domConfiguration.getParameter(name));
+        }
+    }
+
+    public void testSettingImpliedUpdatesInfoset() {
+        for (String name : infosetImpliesFalse) {
+            domConfiguration.setParameter("infoset", true);
+            if (domConfiguration.canSetParameter(name, true)) {
+                domConfiguration.setParameter(name, true);
+                assertEquals(false, domConfiguration.getParameter("infoset"));
+            }
+        }
+
+        for (String name : infosetImpliesTrue) {
+            domConfiguration.setParameter("infoset", true);
+            if (domConfiguration.canSetParameter(name, false)) {
+                domConfiguration.setParameter(name, false);
+                assertEquals(false, domConfiguration.getParameter("infoset"));
+            }
+        }
+    }
+
+    public void testNamespaces() {
+        assertSupported("namespaces", false);
+        assertSupported("namespaces", true);
+    }
+
+    public void testNamespaceDeclarations() {
+        assertUnsupported("namespace-declarations", false); // supported in RI 6
+        assertSupported("namespace-declarations", true);
+    }
+
+    public void testNormalizeCharacters() {
+        assertSupported("normalize-characters", false);
+        assertUnsupported("normalize-characters", true);
+    }
+
+    public void testSchemaLocation() {
+        assertSupported("schema-location", "http://foo");
+        assertSupported("schema-location", null);
+    }
+
+    /**
+     * This fails under the RI because setParameter() succeeds even though
+     * canSetParameter() returns false.
+     */
+    public void testSchemaTypeDtd() {
+        assertUnsupported("schema-type", "http://www.w3.org/TR/REC-xml"); // supported in RI v6
+    }
+
+    public void testSchemaTypeXmlSchema() {
+        assertSupported("schema-type", null);
+        assertSupported("schema-type", "http://www.w3.org/2001/XMLSchema");
+    }
+
+    public void testSplitCdataSections() {
+        assertSupported("split-cdata-sections", false);
+        assertSupported("split-cdata-sections", true);
+    }
+
+    public void testValidate() {
+        assertSupported("validate", false);
+        assertSupported("validate", true);
+    }
+
+    public void testValidateIfSchema() {
+        assertSupported("validate-if-schema", false);
+        assertUnsupported("validate-if-schema", true);
+    }
+
+    public void testWellFormed() {
+        assertSupported("well-formed", false);
+        assertSupported("well-formed", true);
+    }
+
+    public void testMissingParameter() {
+        assertFalse(domConfiguration.canSetParameter("foo", true));
+        try {
+            domConfiguration.getParameter("foo");
+            fail();
+        } catch (DOMException e) {
+        }
+        try {
+            domConfiguration.setParameter("foo", true);
+            fail();
+        } catch (DOMException e) {
+        }
+    }
+
+    public void testNullKey() {
+        try {
+            domConfiguration.canSetParameter(null, true);
+            fail();
+        } catch (NullPointerException e) {
+        }
+        try {
+            domConfiguration.getParameter(null);
+            fail();
+        } catch (NullPointerException e) {
+        }
+        try {
+            domConfiguration.setParameter(null, true);
+            fail();
+        } catch (NullPointerException e) {
+        }
+    }
+
+    public void testNullValue() {
+        String message = "This implementation's canSetParameter() disagrees"
+                + " with its setParameter()";
+        try {
+            domConfiguration.setParameter("well-formed", null);
+            fail(message);
+        } catch (DOMException e) {
+        }
+        assertEquals(message, false, domConfiguration.canSetParameter("well-formed", null));
+    }
+
+    public void testTypeMismatch() {
+        assertEquals(false, domConfiguration.canSetParameter("well-formed", "true"));
+        try {
+            domConfiguration.setParameter("well-formed", "true");
+            fail();
+        } catch (DOMException e) {
+        }
+
+        assertEquals(false, domConfiguration.canSetParameter("well-formed", new Object()));
+        try {
+            domConfiguration.setParameter("well-formed", new Object());
+            fail();
+        } catch (DOMException e) {
+        }
+    }
+
+    private void assertUnsupported(String name, Object value) {
+        String message = "This implementation's setParameter() supports an unexpected value: "
+                + name + "=" + value;
+        assertFalse(message, domConfiguration.canSetParameter(name, value));
+        try {
+            domConfiguration.setParameter(name, value);
+            fail(message);
+        } catch (DOMException e) {
+            assertEquals(DOMException.NOT_SUPPORTED_ERR, e.code);
+        }
+        try {
+            domConfiguration.setParameter(name.toUpperCase(), value);
+            fail(message);
+        } catch (DOMException e) {
+            assertEquals(DOMException.NOT_SUPPORTED_ERR, e.code);
+        }
+        assertFalse(value.equals(domConfiguration.getParameter(name)));
+    }
+
+    private void assertSupported(String name, Object value) {
+        String message = "This implementation's canSetParameter() disagrees"
+                + " with its setParameter() for " + name + "=" + value;
+        try {
+            domConfiguration.setParameter(name, value);
+        } catch (DOMException e) {
+            if (domConfiguration.canSetParameter(name, value)) {
+                fail(message);
+            } else {
+                fail("This implementation's setParameter() doesn't support: "
+                        + name + "=" + value);
+            }
+        }
+        assertTrue(message, domConfiguration.canSetParameter(name.toUpperCase(), value));
+        assertTrue(message, domConfiguration.canSetParameter(name, value));
+        assertEquals(value, domConfiguration.getParameter(name));
+        domConfiguration.setParameter(name.toUpperCase(), value);
+        assertEquals(value, domConfiguration.getParameter(name.toUpperCase()));
+    }
+
+    public void testCdataSectionsNotHonoredByNodeNormalize() throws Exception {
+        String xml = "<foo>ABC<![CDATA[DEF]]>GHI</foo>";
+        document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+                .parse(new InputSource(new StringReader(xml)));
+        document.getDomConfig().setParameter("cdata-sections", true);
+        document.getDocumentElement().normalize();
+        assertEquals(xml, domToString(document));
+
+        document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+                .parse(new InputSource(new StringReader(xml)));
+        document.getDomConfig().setParameter("cdata-sections", false);
+        document.getDocumentElement().normalize();
+        assertEquals(xml, domToString(document));
+    }
+
+    public void testCdataSectionsHonoredByDocumentNormalize() throws Exception {
+        String xml = "<foo>ABC<![CDATA[DEF]]>GHI</foo>";
+        document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+                .parse(new InputSource(new StringReader(xml)));
+        document.getDomConfig().setParameter("cdata-sections", true);
+        document.normalizeDocument();
+        assertEquals(xml, domToString(document));
+
+        document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+                .parse(new InputSource(new StringReader(xml)));
+        document.getDomConfig().setParameter("cdata-sections", false);
+        document.normalizeDocument();
+        String expected = xml.replace("<![CDATA[DEF]]>", "DEF");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testMergeAdjacentTextNodes() throws Exception {
+        document = createDocumentWithAdjacentTexts("abc", "def");
+        document.getDocumentElement().normalize();
+        assertChildren(document.getDocumentElement(), "abcdef");
+    }
+
+    public void testMergeAdjacentEmptyTextNodes() throws Exception {
+        document = createDocumentWithAdjacentTexts("", "", "");
+        document.getDocumentElement().normalize();
+        assertChildren(document.getDocumentElement());
+    }
+
+    public void testMergeAdjacentNodesWithNonTextSiblings() throws Exception {
+        document = createDocumentWithAdjacentTexts("abc", "def", "<br>", "ghi", "jkl");
+        document.getDocumentElement().normalize();
+        assertChildren(document.getDocumentElement(), "abcdef", "<br>", "ghijkl");
+    }
+
+    public void testMergeAdjacentNodesEliminatesEmptyTexts() throws Exception {
+        document = createDocumentWithAdjacentTexts("", "", "<br>", "", "", "<br>", "", "<br>", "");
+        document.getDocumentElement().normalize();
+        assertChildren(document.getDocumentElement(), "<br>", "<br>", "<br>");
+    }
+
+    private Document createDocumentWithAdjacentTexts(String... texts) throws Exception {
+        Document result = DocumentBuilderFactory.newInstance()
+                .newDocumentBuilder().newDocument();
+        Element root = result.createElement("foo");
+        result.appendChild(root);
+        for (String text : texts) {
+            if (text.equals("<br>")) {
+                root.appendChild(result.createElement("br"));
+            } else {
+                root.appendChild(result.createTextNode(text));
+            }
+        }
+        return result;
+    }
+
+    private void assertChildren(Element element, String... texts) {
+        List<String> actual = new ArrayList<String>();
+        NodeList nodes = element.getChildNodes();
+        for (int i = 0; i < nodes.getLength(); i++) {
+            Node node = nodes.item(i);
+            actual.add(node.getNodeType() == Node.TEXT_NODE
+                    ? ((Text) node).getData()
+                    : "<" + node.getNodeName() + ">");
+        }
+        assertEquals(Arrays.asList(texts), actual);
+    }
+
+    private String domToString(Document document) throws TransformerException {
+        StringWriter writer = new StringWriter();
+        TransformerFactory.newInstance().newTransformer()
+                .transform(new DOMSource(document), new StreamResult(writer));
+        String xml = writer.toString();
+        return xml.replaceFirst("<\\?xml[^?]*\\?>", "");
+    }
+}