New implementation for DOMConfiguration.

This API is disgusting. Its not regular, not typesafe, sparsely
implemented and overly specific. I'm implementing the minimum I can
get away with - exactly the same route taken by the RI.

The Parameter stuff feels a little bit like overkill, but it allowed
me to group related chunks of code together and avoid long chains of
equalsIgnoreCase if statements.

This is necessary to implement Document.normalize(), one of the
last remaining APIs in DOMv3.

Also fixing our implementation of Node.normalize() and adding tests
to discover a bug in that code. Previously a document like this:
  "<foo>abc<br>def</foo>"
Would be normalized to this:
  "<foo>abcdef<br></foo>"
Which is a horrible bug! Yuck.

Implementation and tests for Document.normalize() are forthcoming.
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[^?]*\\?>", "");
+    }
+}