You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by oh...@apache.org on 2009/11/05 07:50:35 UTC

svn commit: r833015 - in /commons/proper/configuration/branches/configuration2_experimental/src: main/java/org/apache/commons/configuration2/base/impl/ test/java/org/apache/commons/configuration2/base/impl/

Author: oheger
Date: Thu Nov  5 06:50:34 2009
New Revision: 833015

URL: http://svn.apache.org/viewvc?rev=833015&view=rev
Log:
Initial implementation of XMLConfigurationSource.

Added:
    commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java   (with props)
    commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java   (with props)
    commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java   (with props)

Added: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java?rev=833015&view=auto
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java (added)
+++ commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java Thu Nov  5 06:50:34 2009
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.commons.configuration2.base.impl;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.commons.configuration2.expr.NodeHandler;
+import org.apache.commons.configuration2.expr.NodeVisitorAdapter;
+import org.apache.commons.configuration2.tree.ConfigurationNode;
+
+/**
+ * A specialized visitor base class that can be used by in-memory hierarchical
+ * configuration sources for persisting the tree of configuration nodes.</p>
+ * <p>
+ * The basic idea behind this class is that each node can be associated with a
+ * reference object. This reference object has a concrete meaning in a derived
+ * class, e.g. an entry in a JNDI context or an XML element. When the
+ * configuration node tree is set up, the concrete configuration source
+ * implementation is responsible for setting the reference objects. When the
+ * configuration node tree is later modified, new nodes do not have a defined
+ * reference object. This visitor class processes all nodes and finds the ones
+ * without a defined reference object. For those nodes the {@code insert()}
+ * method is called, which must be defined in concrete sub classes. This method
+ * can perform all steps to integrate the new node into the original structure.
+ * </p>
+ *
+ * @author <a
+ *         href="http://commons.apache.org/configuration/team-list.html">Commons
+ *         Configuration team</a>
+ * @version $Id$
+ */
+public abstract class ConfigurationNodeBuilderVisitor extends
+        NodeVisitorAdapter<ConfigurationNode>
+{
+    /**
+     * Visits the specified node before its children have been traversed.
+     *
+     * @param node the node to visit
+     * @param handler the node handler
+     */
+    @Override
+    public void visitBeforeChildren(ConfigurationNode node,
+            NodeHandler<ConfigurationNode> handler)
+    {
+        Collection<ConfigurationNode> subNodes = new LinkedList<ConfigurationNode>(
+                node.getChildren());
+        subNodes.addAll(node.getAttributes());
+        Iterator<ConfigurationNode> children = subNodes.iterator();
+        ConfigurationNode sibling1 = null;
+        ConfigurationNode nd = null;
+
+        while (children.hasNext())
+        {
+            // find the next new node
+            do
+            {
+                sibling1 = nd;
+                nd = children.next();
+            } while (nd.getReference() != null && children.hasNext());
+
+            if (nd.getReference() == null)
+            {
+                // find all following new nodes
+                List<ConfigurationNode> newNodes = new LinkedList<ConfigurationNode>();
+                newNodes.add(nd);
+                while (children.hasNext())
+                {
+                    nd = children.next();
+                    if (nd.getReference() == null)
+                    {
+                        newNodes.add(nd);
+                    }
+                    else
+                    {
+                        break;
+                    }
+                }
+
+                // Insert all new nodes
+                ConfigurationNode sibling2 = (nd.getReference() == null) ? null
+                        : nd;
+                for (ConfigurationNode insertNode : newNodes)
+                {
+                    if (insertNode.getReference() == null)
+                    {
+                        Object ref = insert(insertNode, node, sibling1,
+                                sibling2);
+                        if (ref != null)
+                        {
+                            insertNode.setReference(ref);
+                        }
+                        sibling1 = insertNode;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Inserts a new node into the structure constructed by this builder. This
+     * method is called for each node that has been added to the configuration
+     * tree after the configuration has been loaded from its source. These new
+     * nodes have to be inserted into the original structure. The passed in
+     * nodes define the position of the node to be inserted: its parent and the
+     * siblings between to insert. The return value is interpreted as the new
+     * reference of the affected <code>Node</code> object; if it is not <b>null
+     * </b>, it is passed to the node's <code>setReference()</code> method.
+     *
+     * @param newNode the node to be inserted
+     * @param parent the parent node
+     * @param sibling1 the sibling after which the node is to be inserted; can
+     *        be <b>null </b> if the new node is going to be the first child
+     *        node
+     * @param sibling2 the sibling before which the node is to be inserted; can
+     *        be <b>null </b> if the new node is going to be the last child node
+     * @return the reference object for the node to be inserted
+     */
+    protected abstract Object insert(ConfigurationNode newNode,
+            ConfigurationNode parent, ConfigurationNode sibling1,
+            ConfigurationNode sibling2);
+}

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/ConfigurationNodeBuilderVisitor.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java?rev=833015&view=auto
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java (added)
+++ commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java Thu Nov  5 06:50:34 2009
@@ -0,0 +1,1177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.commons.configuration2.base.impl;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.commons.configuration2.ConfigurationException;
+import org.apache.commons.configuration2.base.Capability;
+import org.apache.commons.configuration2.base.DefaultLocatorSupport;
+import org.apache.commons.configuration2.base.InMemoryConfigurationSource;
+import org.apache.commons.configuration2.base.LocatorSupport;
+import org.apache.commons.configuration2.base.StreamBasedSource;
+import org.apache.commons.configuration2.expr.ConfigurationNodeHandler;
+import org.apache.commons.configuration2.expr.NodeHandler;
+import org.apache.commons.configuration2.expr.NodeVisitorAdapter;
+import org.apache.commons.configuration2.resolver.DefaultEntityResolver;
+import org.apache.commons.configuration2.resolver.EntityRegistry;
+import org.apache.commons.configuration2.tree.ConfigurationNode;
+import org.apache.commons.configuration2.tree.DefaultConfigurationNode;
+import org.w3c.dom.Attr;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * <p>
+ * A specialized hierarchical in-memory configuration source class that is able
+ * to parse XML documents.
+ * </p>
+ * <p>
+ * The parsed document will be stored keeping its structure. The class also
+ * tries to preserve as much information from the loaded XML document as
+ * possible, including comments and processing instructions. These will be
+ * contained in documents created by the {@code save()} methods, too.
+ * </p>
+ * <p>
+ * Whitespace in the content of XML documents is trimmed per default. In most
+ * cases this is desired. However, sometimes whitespace is indeed important and
+ * should be treated as part of the value of a property as in the following
+ * example:
+ *
+ * <pre>
+ *   &lt;indent&gt;    &lt;/indent&gt;
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * Per default the spaces in the {@code indent} element will be trimmed
+ * resulting in an empty element. To tell {@code XMLConfigurationSource} that
+ * spaces are relevant the {@code xml:space} attribute can be used, which is
+ * defined in the <a href="http://www.w3.org/TR/REC-xml/#sec-white-space">XML
+ * specification</a>. This will look as follows:
+ *
+ * <pre>
+ *   &lt;indent xml:space=&quot;preserve&quot;&gt;    &lt;/indent&gt;
+ * </pre>
+ *
+ * The value of the {@code indent} property will now contain the spaces.
+ * </p>
+ * <p>
+ * {@code XMLConfigurationSource} implements the {@link LocatorSupport}
+ * capability and thus provides full support for loading XML documents from
+ * {@code Locator} objects. A full description of these features can be found in
+ * the documentation of {@link LocatorSupport}.
+ * </p>
+ * <p>
+ * <em>Note:</em>Configuration source objects of this type can be read
+ * concurrently by multiple threads. However, if one of these threads modifies
+ * the object, synchronization has to be performed manually.
+ * </p>
+ *
+ * @author <a
+ *         href="http://commons.apache.org/configuration/team-list.html">Commons
+ *         Configuration team</a>
+ * @version $Id$
+ */
+public class XMLConfigurationSource extends InMemoryConfigurationSource
+        implements StreamBasedSource
+{
+    /** Constant for the default root element name. */
+    private static final String DEFAULT_ROOT_NAME = "configuration";
+
+    /** Constant for the name of the space attribute. */
+    private static final String ATTR_SPACE = "xml:space";
+
+    /** Constant for the xml:space value for preserving whitespace. */
+    private static final String VALUE_PRESERVE = "preserve";
+
+    /** Schema Language key for the parser */
+    private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
+
+    /** Schema Language for the parser */
+    private static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
+
+    /** The node handler used by this instance. */
+    private final NodeHandler<ConfigurationNode> xmlNodeHandler;
+
+    /** The locator support object used by this configuration source. */
+    private final LocatorSupport locatorSupport;
+
+    /** The underlying XML document */
+    private Document document;
+
+    /** Stores the name of the root element. */
+    private String rootElementName;
+
+    /** Stores the public ID from the DOCTYPE. */
+    private String publicID;
+
+    /** Stores the system ID from the DOCTYPE. */
+    private String systemID;
+
+    /** Stores the document builder that should be used for loading. */
+    private DocumentBuilder documentBuilder;
+
+    /** Stores a flag whether DTD or Schema validation should be performed. */
+    private boolean validating;
+
+    /** Stores a flag whether DTD or Schema validation is used. */
+    private boolean schemaValidation;
+
+    /** The EntityResolver to use. */
+    private EntityResolver entityResolver = new DefaultEntityResolver();
+
+    /**
+     * Creates a new instance of {@code XMLConfigurationSource}.
+     */
+    public XMLConfigurationSource()
+    {
+        xmlNodeHandler = new XMLNodeHandler();
+        locatorSupport = new DefaultLocatorSupport(this);
+    }
+
+    /**
+     * Returns the name of the root element. If this configuration source was
+     * loaded from a XML document, the name of this document's root element is
+     * returned. Otherwise, it is possible to set a name for the root element
+     * that will be used when this configuration source is saved.
+     *
+     * @return the name of the root element
+     */
+    public String getRootElementName()
+    {
+        if (getDocument() == null)
+        {
+            return (rootElementName == null) ? DEFAULT_ROOT_NAME
+                    : rootElementName;
+        }
+        else
+        {
+            return getDocument().getDocumentElement().getNodeName();
+        }
+    }
+
+    /**
+     * Sets the name of the root element. This name is used when this
+     * configuration source object is stored in an XML file. Note that setting
+     * the name of the root element works only if this configuration has been
+     * newly created. If the configuration was loaded from an XML file, the name
+     * cannot be changed and an {@code UnsupportedOperationException} exception
+     * is thrown. Whether this configuration has been loaded from an XML
+     * document or not can be found out using the {@code getDocument()} method.
+     *
+     * @param name the name of the root element
+     */
+    public void setRootElementName(String name)
+    {
+        if (getDocument() != null)
+        {
+            throw new UnsupportedOperationException(
+                    "The name of the root element "
+                            + "cannot be changed when loaded from an XML document!");
+        }
+        rootElementName = name;
+        getRootNode().setName(name);
+    }
+
+    /**
+     * Returns the {@code DocumentBuilder} object that is used for loading
+     * documents. If no specific builder has been set, this method returns
+     * <b>null</b>.
+     *
+     * @return the {@code DocumentBuilder} for loading new documents
+     */
+    public DocumentBuilder getDocumentBuilder()
+    {
+        return documentBuilder;
+    }
+
+    /**
+     * Sets the {@code DocumentBuilder} object to be used for loading documents.
+     * This method makes it possible to specify the exact document builder. So
+     * an application can create a builder, configure it for its special needs,
+     * and then pass it to this method.
+     *
+     * @param documentBuilder the {@code DocumentBuilder} to be used; if
+     *        undefined, a default builder will be used
+     */
+    public void setDocumentBuilder(DocumentBuilder documentBuilder)
+    {
+        this.documentBuilder = documentBuilder;
+    }
+
+    /**
+     * Returns the public ID of the DOCTYPE declaration from the loaded XML
+     * document. This is <b>null</b> if no document has been loaded yet or if
+     * the document does not contain a DOCTYPE declaration with a public ID.
+     *
+     * @return the public ID
+     */
+    public String getPublicID()
+    {
+        return publicID;
+    }
+
+    /**
+     * Sets the public ID of the DOCTYPE declaration. When this configuration is
+     * saved, a DOCTYPE declaration will be constructed that contains this
+     * public ID.
+     *
+     * @param publicID the public ID
+     */
+    public void setPublicID(String publicID)
+    {
+        this.publicID = publicID;
+    }
+
+    /**
+     * Returns the system ID of the DOCTYPE declaration from the loaded XML
+     * document. This is <b>null</b> if no document has been loaded yet or if
+     * the document does not contain a DOCTYPE declaration with a system ID.
+     *
+     * @return the system ID
+     */
+    public String getSystemID()
+    {
+        return systemID;
+    }
+
+    /**
+     * Sets the system ID of the DOCTYPE declaration. When this configuration is
+     * saved, a DOCTYPE declaration will be constructed that contains this
+     * system ID.
+     *
+     * @param systemID the system ID
+     */
+    public void setSystemID(String systemID)
+    {
+        this.systemID = systemID;
+    }
+
+    /**
+     * Returns the value of the validating flag.
+     *
+     * @return the validating flag
+     */
+    public boolean isValidating()
+    {
+        return validating;
+    }
+
+    /**
+     * Sets the value of the validating flag. This flag determines whether
+     * DTD/Schema validation should be performed when loading XML documents.
+     * This flag is evaluated only if no custom {@code DocumentBuilder} was set.
+     *
+     * @param validating the validating flag
+     */
+    public void setValidating(boolean validating)
+    {
+        if (!schemaValidation)
+        {
+            this.validating = validating;
+        }
+    }
+
+    /**
+     * Returns the value of the schemaValidation flag.
+     *
+     * @return the schemaValidation flag
+     */
+    public boolean isSchemaValidation()
+    {
+        return schemaValidation;
+    }
+
+    /**
+     * Sets the value of the schemaValidation flag. This flag determines whether
+     * DTD or Schema validation should be used. This flag is evaluated only if
+     * no custom {@code DocumentBuilder} was set. If set to true, the XML
+     * document must contain a schemaLocation definition that provides
+     * resolvable hints to the required schemas.
+     *
+     * @param schemaValidation the validating flag
+     */
+    public void setSchemaValidation(boolean schemaValidation)
+    {
+        this.schemaValidation = schemaValidation;
+        if (schemaValidation)
+        {
+            this.validating = true;
+        }
+    }
+
+    /**
+     * Sets a new EntityResolver. Setting this will cause RegisterEntityId to
+     * have no effect.
+     *
+     * @param resolver The EntityResolver to use.
+     */
+    public void setEntityResolver(EntityResolver resolver)
+    {
+        this.entityResolver = resolver;
+    }
+
+    /**
+     * Returns the EntityResolver.
+     *
+     * @return The EntityResolver.
+     */
+    public EntityResolver getEntityResolver()
+    {
+        return this.entityResolver;
+    }
+
+    /**
+     * <p>
+     * Registers the specified DTD URL for the specified public identifier.
+     * </p>
+     * <p>
+     * {@code XMLConfigurationSource} contains an internal {@code
+     * EntityResolver} implementation. This maps {@code PUBLICID}'s to URLs
+     * (from which the resource will be loaded). A common use case for this
+     * method is to register local URLs (possibly computed at runtime by a class
+     * loader) for DTDs. This allows the performance advantage of using a local
+     * version without having to ensure every {@code SYSTEM} URI on every
+     * processed XML document is local. This implementation provides only basic
+     * functionality. If more sophisticated features are required, using
+     * {@link #setDocumentBuilder(DocumentBuilder)} to set a custom {@code
+     * DocumentBuilder} (which also can be initialized with a custom {@code
+     * EntityResolver}) is recommended.
+     * </p>
+     * <p>
+     * <strong>Note:</strong> This method will have no effect if a custom
+     * {@code DocumentBuilder} has been set. (Setting a custom {@code
+     * DocumentBuilder} overrides the internal implementation.)
+     * </p>
+     *
+     * @param publicId Public identifier of the DTD to be resolved
+     * @param entityURL The URL to use for reading this DTD
+     * @throws IllegalArgumentException if the public ID is undefined
+     * @since 1.5
+     */
+    public void registerEntityId(String publicId, URL entityURL)
+    {
+        if (entityResolver instanceof EntityRegistry)
+        {
+            ((EntityRegistry) entityResolver).registerEntityId(publicId,
+                    entityURL);
+        }
+    }
+
+    /**
+     * Returns the XML document this configuration source was loaded from. The
+     * return value is <b>null</b> if this configuration source was not loaded
+     * from a XML document.
+     *
+     * @return the XML document this configuration was loaded from
+     */
+    public Document getDocument()
+    {
+        return document;
+    }
+
+    /**
+     * Returns the {@code NodeHandler} used by this {@code
+     * HierarchicalConfigurationSource}. This implementation returns a special
+     * node handler for dealing with the XML nodes used internally.
+     *
+     * @return the {@code NodeHandler}
+     */
+    @Override
+    public NodeHandler<ConfigurationNode> getNodeHandler()
+    {
+        return xmlNodeHandler;
+    }
+
+    /**
+     * Removes all properties from this configuration source. If this
+     * configuration source was loaded from a XML document, the associated DOM
+     * document is also cleared.
+     */
+    @Override
+    public void clear()
+    {
+        super.clear();
+        document = null;
+    }
+
+    /**
+     * Initializes this configuration source from an XML document. The elements
+     * in the XML document are traversed and their data is added to the content
+     * of this {@code ConfigurationSource}.
+     *
+     * @param document the document to be parsed
+     * @param elemRefs a flag whether references to the XML elements should be
+     *        set
+     */
+    public void initFromDocument(Document document, boolean elemRefs)
+    {
+        if (document.getDoctype() != null)
+        {
+            setPublicID(document.getDoctype().getPublicId());
+            setSystemID(document.getDoctype().getSystemId());
+        }
+
+        constructHierarchy(getRootNode(), document.getDocumentElement(),
+                elemRefs, true);
+        getRootNode().setName(document.getDocumentElement().getNodeName());
+        if (elemRefs)
+        {
+            getRootNode().setReference(document.getDocumentElement());
+        }
+    }
+
+    /**
+     * Loads the data from the specified {@code Reader} and adds it to this
+     * {@code ConfigurationSource} object. Note that the{@code clear()} method
+     * is not called, so the properties contained in the loaded file will be
+     * added to the current set of properties.
+     *
+     * @param reader the reader to be read
+     * @throws IOException if an I/O error occurs
+     * @throws ConfigurationException if an error occurs
+     */
+    public void load(Reader reader) throws IOException, ConfigurationException
+    {
+        load(new InputSource(reader));
+    }
+
+    /**
+     * Writes the content of this configuration source to the specified {@code
+     * Writer}. This implementation calls {@link #createTransformer()} for
+     * obtaining a transformer. This transformer is then used to serialize the
+     * internal document to XML.
+     *
+     * @param writer the writer
+     * @throws IOException if an I/O error occurs
+     * @throws ConfigurationException if an error occurs
+     */
+    public void save(Writer writer) throws IOException, ConfigurationException
+    {
+        try
+        {
+            Transformer transformer = createTransformer();
+            Source source = new DOMSource(createDocument());
+            Result result = new StreamResult(writer);
+            transformer.transform(source, result);
+        }
+        catch (TransformerException e)
+        {
+            throw new ConfigurationException(
+                    "Unable to save the configuration", e);
+        }
+        catch (TransformerFactoryConfigurationError err)
+        {
+            throw new ConfigurationException(
+                    "Unable to save the configuration", err);
+        }
+    }
+
+    /**
+     * Validate the document against the Schema.
+     *
+     * @throws ConfigurationException if the validation fails.
+     */
+    public void validate() throws ConfigurationException
+    {
+        try
+        {
+            Transformer transformer = createTransformer();
+            Source source = new DOMSource(createDocument());
+            StringWriter writer = new StringWriter();
+            Result result = new StreamResult(writer);
+            transformer.transform(source, result);
+            Reader reader = new StringReader(writer.getBuffer().toString());
+            DocumentBuilder builder = createDocumentBuilder();
+            builder.parse(new InputSource(reader));
+        }
+        catch (SAXException e)
+        {
+            throw new ConfigurationException("Validation failed", e);
+        }
+        catch (IOException e)
+        {
+            throw new ConfigurationException("Validation failed", e);
+        }
+        catch (TransformerException e)
+        {
+            throw new ConfigurationException("Validation failed", e);
+        }
+        catch (ParserConfigurationException pce)
+        {
+            throw new ConfigurationException("Validation failed", pce);
+        }
+    }
+
+    /**
+     * Creates the {@code DocumentBuilder} to be used for loading XML documents.
+     * This implementation checks whether a specific {@code DocumentBuilder} has
+     * been set. If this is the case, it is used directly. Otherwise a default
+     * builder is created. Depending on the value of the validating flag this
+     * builder will be a validating or a non validating {@code DocumentBuilder}.
+     *
+     * @return the {@code DocumentBuilder} for loading XML configuration files
+     * @throws ParserConfigurationException if an error occurs
+     */
+    protected DocumentBuilder createDocumentBuilder()
+            throws ParserConfigurationException
+    {
+        if (getDocumentBuilder() != null)
+        {
+            return getDocumentBuilder();
+        }
+        else
+        {
+            DocumentBuilderFactory factory = DocumentBuilderFactory
+                    .newInstance();
+            if (isValidating())
+            {
+                factory.setValidating(true);
+                if (isSchemaValidation())
+                {
+                    factory.setNamespaceAware(true);
+                    factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
+                }
+            }
+
+            DocumentBuilder result = factory.newDocumentBuilder();
+            result.setEntityResolver(this.entityResolver);
+
+            if (isValidating())
+            {
+                // register an error handler which detects validation errors
+                result.setErrorHandler(new DefaultHandler()
+                {
+                    @Override
+                    public void error(SAXParseException ex) throws SAXException
+                    {
+                        throw ex;
+                    }
+                });
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Creates a DOM document from the internal tree of configuration nodes.
+     *
+     * @return the new document
+     * @throws ConfigurationException if an error occurs
+     */
+    protected Document createDocument() throws ConfigurationException
+    {
+        try
+        {
+            if (document == null)
+            {
+                DocumentBuilder builder = DocumentBuilderFactory.newInstance()
+                        .newDocumentBuilder();
+                Document newDocument = builder.newDocument();
+                Element rootElem = newDocument
+                        .createElement(getRootElementName());
+                newDocument.appendChild(rootElem);
+                document = newDocument;
+            }
+
+            XMLBuilderVisitor builder = new XMLBuilderVisitor(document);
+            NodeVisitorAdapter.visit(builder, getRootNode(), getNodeHandler());
+            initRootElementText(document, getRootNode().getValue());
+            return document;
+        }
+        catch (DOMException domEx)
+        {
+            throw new ConfigurationException(domEx);
+        }
+        catch (ParserConfigurationException pex)
+        {
+            throw new ConfigurationException(pex);
+        }
+    }
+
+    /**
+     * Creates and initializes the transformer used for save operations. This
+     * base implementation initializes all of the default settings like
+     * indentation mode and the DOCTYPE. Derived classes may overload this
+     * method if they have specific needs.
+     *
+     * @return the transformer to use for a save operation
+     * @throws TransformerException if an error occurs
+     */
+    protected Transformer createTransformer() throws TransformerException
+    {
+        Transformer transformer = TransformerFactory.newInstance()
+                .newTransformer();
+
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        if (locatorSupport.getEncoding() != null)
+        {
+            transformer.setOutputProperty(OutputKeys.ENCODING, locatorSupport
+                    .getEncoding());
+        }
+        if (getPublicID() != null)
+        {
+            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
+                    getPublicID());
+        }
+        if (getSystemID() != null)
+        {
+            transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
+                    getSystemID());
+        }
+
+        return transformer;
+    }
+
+    /**
+     * Defines additional capabilities used by this {@code ConfigurationSource}.
+     * This implementation adds a {@code LocatorSupport} capability.
+     *
+     * @param caps the collection with additional capabilities
+     */
+    @Override
+    protected void appendCapabilities(Collection<Capability> caps)
+    {
+        caps.add(new Capability(LocatorSupport.class, locatorSupport));
+    }
+
+    /**
+     * Helper method for building the internal storage hierarchy. The XML
+     * elements are transformed into node objects.
+     *
+     * @param node the actual node
+     * @param element the actual XML element
+     * @param elemRefs a flag whether references to the XML elements should be
+     *        set
+     * @param trim a flag whether the text content of elements should be
+     *        trimmed; this controls the whitespace handling
+     */
+    private void constructHierarchy(ConfigurationNode node, Element element,
+            boolean elemRefs, boolean trim)
+    {
+        boolean trimFlag = shouldTrim(element, trim);
+        processAttributes(node, element, elemRefs);
+        StringBuilder buffer = new StringBuilder();
+        NodeList list = element.getChildNodes();
+        for (int i = 0; i < list.getLength(); i++)
+        {
+            org.w3c.dom.Node w3cNode = list.item(i);
+            if (w3cNode instanceof Element)
+            {
+                Element child = (Element) w3cNode;
+                ConfigurationNode childNode = new XMLNode(null, child
+                        .getTagName());
+                if (elemRefs)
+                {
+                    childNode.setReference(child);
+                }
+                constructHierarchy(childNode, child, elemRefs, trimFlag);
+                node.addChild(childNode);
+            }
+            else if (w3cNode instanceof Text)
+            {
+                Text data = (Text) w3cNode;
+                buffer.append(data.getData());
+            }
+        }
+
+        String text = buffer.toString();
+        if (trimFlag)
+        {
+            text = text.trim();
+        }
+        if (text.length() > 0 || !hasChildren(node))
+        {
+            node.setValue(text);
+        }
+    }
+
+    /**
+     * Helper method for constructing node objects for the attributes of the
+     * given XML element.
+     *
+     * @param node the current node
+     * @param element the current XML element
+     * @param elemRefs a flag whether references to the XML elements should be
+     *        set
+     */
+    private void processAttributes(ConfigurationNode node, Element element,
+            boolean elemRefs)
+    {
+        NamedNodeMap attributes = element.getAttributes();
+        for (int i = 0; i < attributes.getLength(); ++i)
+        {
+            org.w3c.dom.Node w3cNode = attributes.item(i);
+            if (w3cNode instanceof Attr)
+            {
+                Attr attr = (Attr) w3cNode;
+                ConfigurationNode child = new XMLNode(null, attr.getName());
+                child.setValue(attr.getValue());
+                if (elemRefs)
+                {
+                    child.setReference(element);
+                }
+                node.addAttribute(child);
+            }
+        }
+    }
+
+    /**
+     * Loads an XML document from the specified input source.
+     *
+     * @param source the input source
+     * @throws ConfigurationException if an error occurs
+     */
+    private void load(InputSource source) throws IOException,
+            ConfigurationException
+    {
+        try
+        {
+            if (locatorSupport.getLocator() != null)
+            {
+                source.setSystemId(locatorSupport.getLocator().getURL(false)
+                        .toString());
+            }
+
+            DocumentBuilder builder = createDocumentBuilder();
+            Document newDocument = builder.parse(source);
+            Document oldDocument = document;
+            document = null;
+            initFromDocument(newDocument, oldDocument == null);
+            document = (oldDocument == null) ? newDocument : oldDocument;
+        }
+        catch (SAXException spe)
+        {
+            throw new ConfigurationException("Error parsing "
+                    + source.getSystemId(), spe);
+        }
+        catch (ParserConfigurationException pex)
+        {
+            throw new ConfigurationException(
+                    "Error when creating document builder", pex);
+        }
+    }
+
+    /**
+     * Sets the text of the root element of a newly created XML Document.
+     *
+     * @param doc the document
+     * @param value the new text to be set
+     */
+    private void initRootElementText(Document doc, Object value)
+    {
+        Element elem = doc.getDocumentElement();
+        NodeList children = elem.getChildNodes();
+
+        // Remove all existing text nodes
+        for (int i = 0; i < children.getLength(); i++)
+        {
+            org.w3c.dom.Node nd = children.item(i);
+            if (nd.getNodeType() == org.w3c.dom.Node.TEXT_NODE)
+            {
+                elem.removeChild(nd);
+            }
+        }
+
+        if (value != null)
+        {
+            // Add a new text node
+            elem.appendChild(doc.createTextNode(String.valueOf(value)));
+        }
+    }
+
+    /**
+     * Tests whether the specified node has some child elements.
+     *
+     * @param node the node to check
+     * @return a flag whether there are child elements
+     */
+    private static boolean hasChildren(ConfigurationNode node)
+    {
+        return node.getChildrenCount() > 0 || node.getAttributeCount() > 0;
+    }
+
+    /**
+     * Checks whether the content of the current XML element should be trimmed.
+     * This method checks whether a <code>xml:space</code> attribute is present
+     * and evaluates its value. See <a
+     * href="http://www.w3.org/TR/REC-xml/#sec-white-space">
+     * http://www.w3.org/TR/REC-xml/#sec-white-space</a> for more details.
+     *
+     * @param element the current XML element
+     * @param currentTrim the current trim flag
+     * @return a flag whether the content of this element should be trimmed
+     */
+    private static boolean shouldTrim(Element element, boolean currentTrim)
+    {
+        Attr attr = element.getAttributeNode(ATTR_SPACE);
+
+        if (attr == null)
+        {
+            return currentTrim;
+        }
+        else
+        {
+            return !VALUE_PRESERVE.equals(attr.getValue());
+        }
+    }
+
+    /**
+     * A specialized {@code ConfigurationNode} implementation class that is
+     * connected with an XML element. Changes on a node are also performed on
+     * the associated element.
+     */
+    private class XMLNode extends DefaultConfigurationNode
+    {
+        /**
+         * The serial version UID.
+         */
+        private static final long serialVersionUID = -4133988932174596562L;
+
+        /**
+         * Creates a new instance of {@code XMLNode}.
+         *
+         * @param parent the parent node
+         * @param name the name of this node
+         */
+        public XMLNode(ConfigurationNode parent, String name)
+        {
+            super(name);
+            setParentNode(parent);
+        }
+
+        /**
+         * Sets the value of this node. If this node is associated with an XML
+         * element, this element will be updated, too.
+         *
+         * @param value the node's new value
+         */
+        @Override
+        public void setValue(Object value)
+        {
+            super.setValue(value);
+
+            if (getReference() != null && document != null)
+            {
+                if (isAttribute())
+                {
+                    updateAttribute();
+                }
+                else
+                {
+                    updateElement(value);
+                }
+            }
+        }
+
+        /**
+         * Updates the associated XML elements when a node is removed.
+         */
+        @Override
+        protected void removeReference()
+        {
+            if (getReference() != null)
+            {
+                Element element = (Element) getReference();
+                if (isAttribute())
+                {
+                    updateAttribute();
+                }
+                else
+                {
+                    org.w3c.dom.Node parentElem = element.getParentNode();
+                    if (parentElem != null)
+                    {
+                        parentElem.removeChild(element);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Updates the node's value if it represents an element node.
+         *
+         * @param value the new value
+         */
+        private void updateElement(Object value)
+        {
+            Text txtNode = findTextNodeForUpdate();
+            if (value == null)
+            {
+                // remove text
+                if (txtNode != null)
+                {
+                    ((Element) getReference()).removeChild(txtNode);
+                }
+            }
+            else
+            {
+                if (txtNode == null)
+                {
+                    String newValue = value.toString();
+                    txtNode = document.createTextNode(newValue);
+                    if (((Element) getReference()).getFirstChild() != null)
+                    {
+                        ((Element) getReference()).insertBefore(txtNode,
+                                ((Element) getReference()).getFirstChild());
+                    }
+                    else
+                    {
+                        ((Element) getReference()).appendChild(txtNode);
+                    }
+                }
+                else
+                {
+                    txtNode.setNodeValue(value.toString());
+                }
+            }
+        }
+
+        /**
+         * Updates the node's value if it represents an attribute.
+         */
+        private void updateAttribute()
+        {
+            XMLBuilderVisitor.updateAttribute(getParentNode(), getName());
+        }
+
+        /**
+         * Returns the only text node of this element for update. This method is
+         * called when the element's text changes. Then all text nodes except
+         * for the first are removed. A reference to the first is returned or
+         * <b>null </b> if there is no text node at all.
+         *
+         * @return the first and only text node
+         */
+        private Text findTextNodeForUpdate()
+        {
+            Text result = null;
+            Element elem = (Element) getReference();
+            // Find all Text nodes
+            NodeList children = elem.getChildNodes();
+            Collection<org.w3c.dom.Node> textNodes = new ArrayList<org.w3c.dom.Node>();
+            for (int i = 0; i < children.getLength(); i++)
+            {
+                org.w3c.dom.Node nd = children.item(i);
+                if (nd instanceof Text)
+                {
+                    if (result == null)
+                    {
+                        result = (Text) nd;
+                    }
+                    else
+                    {
+                        textNodes.add(nd);
+                    }
+                }
+            }
+
+            // We don't want CDATAs
+            if (result instanceof CDATASection)
+            {
+                textNodes.add(result);
+                result = null;
+            }
+
+            // Remove all but the first Text node
+            for (org.w3c.dom.Node child : textNodes)
+            {
+                elem.removeChild(child);
+            }
+            return result;
+        }
+    }
+
+    /**
+     * A specialized {@code NodeHandler} implementation for the nodes used by an
+     * {@code XMLConfigurationSource}. This class differs from its base class in
+     * the type of nodes that it creates: it uses the special node class {@code
+     * XMLNode}.
+     */
+    private class XMLNodeHandler extends ConfigurationNodeHandler
+    {
+        @Override
+        protected ConfigurationNode createNode(ConfigurationNode parent,
+                String name)
+        {
+            return new XMLNode(parent, name);
+        }
+    }
+
+    /**
+     * A concrete {@code ConfigurationNodeBuilderVisitor} implementation that
+     * can construct XML documents.
+     */
+    private static class XMLBuilderVisitor extends
+            ConfigurationNodeBuilderVisitor
+    {
+        /** Stores the document to be constructed. */
+        private Document document;
+
+        /**
+         * Creates a new instance of {@code XMLBuilderVisitor}.
+         *
+         * @param doc the document to be created
+         */
+        public XMLBuilderVisitor(Document doc)
+        {
+            document = doc;
+        }
+
+        /**
+         * Inserts a new node. This implementation ensures that the correct XML
+         * element is created and inserted between the given siblings.
+         *
+         * @param newNode the node to insert
+         * @param parent the parent node
+         * @param sibling1 the first sibling
+         * @param sibling2 the second sibling
+         * @return the new node
+         */
+        @Override
+        protected Object insert(ConfigurationNode newNode,
+                ConfigurationNode parent, ConfigurationNode sibling1,
+                ConfigurationNode sibling2)
+        {
+            if (newNode.isAttribute())
+            {
+                updateAttribute(parent, getElement(parent), newNode.getName());
+                return null;
+            }
+
+            else
+            {
+                Element elem = document.createElement(newNode.getName());
+                if (newNode.getValue() != null)
+                {
+                    String txt = newNode.getValue().toString();
+                    elem.appendChild(document.createTextNode(txt));
+                }
+                if (sibling2 == null)
+                {
+                    getElement(parent).appendChild(elem);
+                }
+                else if (sibling1 != null)
+                {
+                    getElement(parent).insertBefore(elem,
+                            getElement(sibling1).getNextSibling());
+                }
+                else
+                {
+                    getElement(parent).insertBefore(elem,
+                            getElement(parent).getFirstChild());
+                }
+                return elem;
+            }
+        }
+
+        /**
+         * Helper method for updating the value of the specified node's
+         * attribute with the given name.
+         *
+         * @param node the affected node
+         * @param elem the element that is associated with this node
+         * @param name the name of the affected attribute
+         */
+        private static void updateAttribute(ConfigurationNode node,
+                Element elem, String name)
+        {
+            if (node != null && elem != null)
+            {
+                String attrValue = null;
+                List<ConfigurationNode> attributes = node.getAttributes(name);
+                if (attributes.size() > 0)
+                {
+                    ConfigurationNode attr = attributes.get(0);
+                    if (attr.getValue() != null)
+                    {
+                        attrValue = attr.getValue().toString();
+                    }
+                }
+
+                if (attrValue == null)
+                {
+                    elem.removeAttribute(name);
+                }
+                else
+                {
+                    elem.setAttribute(name, attrValue);
+                }
+            }
+        }
+
+        /**
+         * Updates the value of the specified attribute of the given node.
+         *
+         * @param node the affected node
+         * @param name the name of the attribute
+         */
+        static void updateAttribute(ConfigurationNode node, String name)
+        {
+            if (node != null)
+            {
+                updateAttribute(node, (Element) node.getReference(), name);
+            }
+        }
+
+        /**
+         * Helper method for accessing the element of the specified node.
+         *
+         * @param node the node
+         * @return the element of this node
+         */
+        private Element getElement(ConfigurationNode node)
+        {
+            // special treatment for root node of the hierarchy
+            return (node.getName() != null && node.getReference() != null) ? (Element) node
+                    .getReference()
+                    : document.getDocumentElement();
+        }
+    }
+}

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/XMLConfigurationSource.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java?rev=833015&view=auto
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java (added)
+++ commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java Thu Nov  5 06:50:34 2009
@@ -0,0 +1,985 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.commons.configuration2.base.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.URL;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.commons.configuration2.ConfigurationAssert;
+import org.apache.commons.configuration2.ConfigurationException;
+import org.apache.commons.configuration2.base.Configuration;
+import org.apache.commons.configuration2.base.ConfigurationImpl;
+import org.apache.commons.configuration2.base.LocatorSupport;
+import org.apache.commons.configuration2.expr.xpath.XPathExpressionEngine;
+import org.apache.commons.configuration2.fs.Locator;
+import org.apache.commons.configuration2.fs.URLLocator;
+import org.apache.commons.configuration2.resolver.CatalogResolver;
+import org.apache.commons.configuration2.tree.ConfigurationNode;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Test class for {@code XMLConfigurationSource}.
+ *
+ * @author <a
+ *         href="http://commons.apache.org/configuration/team-list.html">Commons
+ *         Configuration team</a>
+ * @version $Id$
+ */
+public class TestXMLConfigurationSource
+{
+    /** Constant for the name of the test output file. */
+    private static final String OUT_FILE = "testsave.xml";
+
+    /** Constant for the test encoding. */
+    private static final String ENCODING = "ISO-8859-1";
+
+    /** Constant for the test system ID. */
+    private static final String SYSTEM_ID = "properties.dtd";
+
+    /** Constant for the test public ID. */
+    private static final String PUBLIC_ID = "-//Commons Configuration//DTD Test Configuration 1.3//EN";
+
+    /** Constant for the DOCTYPE declaration. */
+    private static final String DOCTYPE_DECL = " PUBLIC \"" + PUBLIC_ID
+            + "\" \"" + SYSTEM_ID + "\">";
+
+    /** Constant for the DOCTYPE prefix. */
+    private static final String DOCTYPE = "<!DOCTYPE ";
+
+    /** Constant for the transformer factory property. */
+    private static final String PROP_FACTORY = "javax.xml.transform.TransformerFactory";
+
+    /** XML Catalog */
+    private static final String CATALOG_FILES = ConfigurationAssert
+            .getTestFile("catalog.xml").getAbsolutePath();
+
+    /** The locator for the test XML file. */
+    private static Locator testFileLocator;
+
+    /** The locator for the test output file. */
+    private static Locator testOutLocator;
+
+    /** A configuration wrapping the source for convenient access to properties. */
+    private Configuration<ConfigurationNode> conf;
+
+    /** The configuration source to be tested. */
+    private XMLConfigurationSource source;
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception
+    {
+        testFileLocator = new URLLocator(ConfigurationAssert
+                .getTestURL("test.xml"));
+        testOutLocator = new URLLocator(ConfigurationAssert.getOutURL(OUT_FILE));
+    }
+
+    @Before
+    public void setUp() throws Exception
+    {
+        source = new XMLConfigurationSource();
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(testFileLocator);
+        locSupport.load();
+        conf = new ConfigurationImpl<ConfigurationNode>(source);
+        removeTestFile();
+    }
+
+    /**
+     * Removes the test output file if it exists.
+     */
+    private void removeTestFile()
+    {
+        File testSaveConf = ConfigurationAssert.getOutFile(OUT_FILE);
+        if (testSaveConf.exists())
+        {
+            assertTrue(testSaveConf.delete());
+        }
+    }
+
+    /**
+     * Stores the test configuration and loads it again. This is used for
+     * testing the save() facilities and to check whether all kind of properties
+     * can be persisted.
+     *
+     * @return the newly loaded configuration
+     * @throws ConfigurationException in case of an error
+     */
+    private Configuration<ConfigurationNode> reload()
+            throws ConfigurationException
+    {
+        LocatorSupport locSupport = conf.getConfigurationSource()
+                .getCapability(LocatorSupport.class);
+        locSupport.save(testOutLocator);
+        XMLConfigurationSource source2 = new XMLConfigurationSource();
+        LocatorSupport locSupport2 = source2
+                .getCapability(LocatorSupport.class);
+        locSupport2.setLocator(testOutLocator);
+        locSupport2.load();
+        return new ConfigurationImpl<ConfigurationNode>(source2);
+    }
+
+    /**
+     * Tests simple property access.
+     */
+    @Test
+    public void testGetProperty()
+    {
+        assertEquals("Wrong value", "value", conf.getProperty("element"));
+    }
+
+    /**
+     * Tests whether comments are correctly processed.
+     */
+    @Test
+    public void testGetCommentedProperty()
+    {
+        assertEquals("Wrong value for commented property", "", conf
+                .getProperty("test.comment"));
+    }
+
+    /**
+     * Tests whether XML entities are correctly resolved.
+     */
+    @Test
+    public void testGetPropertyWithXMLEntity()
+    {
+        assertEquals("Wrong value for entity", "1<2", conf
+                .getProperty("test.entity"));
+    }
+
+    @Test
+    public void testClearPropertyNonExisting()
+    {
+        String key = "clearly";
+        conf.clearProperty(key);
+        assertNull(key, conf.getProperty(key));
+    }
+
+    /**
+     * Tests whether a non-leaf XML element can be queried. It should not have a
+     * value.
+     */
+    @Test
+    public void testGetPropertyNonLeaf()
+    {
+        Object property = conf.getProperty("clear");
+        assertNull("Got a value", property);
+    }
+
+    @Test
+    public void testGetPropertyNonExisting()
+    {
+        assertNull("Got value for non-existing property (1)", conf
+                .getProperty("e"));
+        assertNull("Got value for non-existing property (2)", conf
+                .getProperty("element3[@n]"));
+    }
+
+    /**
+     * Tests whether an attribute value can be queried.
+     */
+    @Test
+    public void testGetPropertySingleAttribute()
+    {
+        Object property = conf.getProperty("element3[@name]");
+        assertNotNull("No value", property);
+        assertTrue("Wrong type", property instanceof String);
+        assertEquals("Wrong value", "foo", property);
+    }
+
+    /**
+     * Tests whether the value of a CDATA section can be queried.
+     */
+    @Test
+    public void testGetPropertyCData()
+    {
+        Object property = conf.getProperty("test.cdata");
+        assertNotNull("No value", property);
+        assertTrue("Wrong type", property instanceof String);
+        assertEquals("Wrong value", "<cdata value>", property);
+    }
+
+    /**
+     * Tests whether a list of values can be queried.
+     */
+    @Test
+    public void testGetPropertyMultipleSiblings()
+    {
+        Object property = conf.getProperty("list.sublist.item");
+        assertNotNull("No value", property);
+        assertTrue("Not a list", property instanceof List<?>);
+        List<?> list = (List<?>) property;
+        assertEquals("Wrong size", 2, list.size());
+        assertEquals("Wrong element at 0", "five", list.get(0));
+        assertEquals("Wrong element at 1", "six", list.get(1));
+    }
+
+    /**
+     * Tests whether a list consisting of multiple disjoined elements can be
+     * queried.
+     */
+    @Test
+    public void testGetPropertyMultipleDisjoined()
+    {
+        Object property = conf.getProperty("list.item");
+        assertNotNull("No value", property);
+        assertTrue("Not a list", property instanceof List<?>);
+        List<?> list = (List<?>) property;
+        assertEquals("Wrong size", 4, list.size());
+        assertEquals("Wrong element at 0", "one", list.get(0));
+        assertEquals("Wrong element at 1", "two", list.get(1));
+        assertEquals("Wrong element at 2", "three", list.get(2));
+        assertEquals("Wrong element at 3", "four", list.get(3));
+    }
+
+    /**
+     * Tests whether the attributes of list elements can be queried.
+     */
+    @Test
+    public void testGetPropertyMultipleDisjoinedAttributes()
+    {
+        Object property = conf.getProperty("list.item[@name]");
+        assertNotNull("No value", property);
+        assertTrue("Not a list", property instanceof List<?>);
+        List<?> list = (List<?>) property;
+        assertEquals("Wrong size", 2, list.size());
+        assertEquals("Wrong element at 0", "one", list.get(0));
+        assertEquals("Wrong element at 0", "three", list.get(1));
+    }
+
+    /**
+     * Tests whether a deeply nested property can be accessed.
+     */
+    @Test
+    public void testGetPropertyComplex()
+    {
+        assertEquals("Wrong value", "I'm complex!", conf
+                .getProperty("element2.subelement.subsubelement"));
+    }
+
+    /**
+     * Tests access to tag names with delimiter characters.
+     */
+    @Test
+    public void testGetPropertyComplexNames()
+    {
+        assertEquals("Name with dot", conf.getString("complexNames.my..elem"));
+        assertEquals("Another dot", conf
+                .getString("complexNames.my..elem.sub..elem"));
+    }
+
+    /**
+     * Tests the handling of empty elements.
+     */
+    @Test
+    public void testGetPropertyEmptyElements() throws ConfigurationException
+    {
+        assertTrue("Empty key not found", conf.containsKey("empty"));
+        assertEquals("Wrong value of empty property", "", conf
+                .getString("empty"));
+        conf.addProperty("empty2", "");
+        conf.setProperty("empty", "no more empty");
+        Configuration<ConfigurationNode> conf2 = reload();
+        assertEquals("Wrong value after save 1", "no more empty", conf2
+                .getString("empty"));
+        assertEquals("Wrong value after save 2", "", conf2
+                .getProperty("empty2"));
+    }
+
+    /**
+     * Tests whether properties can be accessed if the XPATH expression engine
+     * is set.
+     */
+    @Test
+    public void testGetPropertyXPathExpressionEngine()
+    {
+        conf.setExpressionEngine(new XPathExpressionEngine());
+        assertEquals("Wrong attribute value", "foo\"bar", conf
+                .getString("test[1]/entity/@name"));
+        conf.clear();
+        assertNull("Value still found", conf.getString("test[1]/entity/@name"));
+    }
+
+    /**
+     * Tests list nodes with multiple values and attributes.
+     */
+    @Test
+    public void testGetPropertyListWithAttributes()
+    {
+        assertEquals("Wrong number of <a> elements", 3, conf.getList(
+                "attrList.a").size());
+        assertEquals("Wrong value of first element", "ABC", conf
+                .getString("attrList.a(0)"));
+        assertEquals("Wrong value of 2nd element", "1,2,3", conf
+                .getString("attrList.a(1)"));
+        assertEquals("Wrong value of first name attribute", "x", conf
+                .getString("attrList.a(0)[@name]"));
+        assertEquals("Wrong value of 2nd name attribute", "y", conf
+                .getString("attrList.a(1)[@name]"));
+        assertEquals("Wrong number of name attributes", 3, conf.getList(
+                "attrList.a[@name]").size());
+    }
+
+    /**
+     * Tests a list node with multiple attributes.
+     */
+    @Test
+    public void testGetPropertyListWithMultiAttributesMultiValue()
+    {
+        assertEquals("Wrong value of list element", "value1,value2", conf
+                .getString("attrList.a(2)"));
+        assertEquals("Wrong value of test attribute", "yes", conf
+                .getString("attrList.a(2)[@test]"));
+        assertEquals("Wrong value of name attribute", "u,v,w", conf
+                .getString("attrList.a(2)[@name]"));
+    }
+
+    @Test
+    public void testClearPropertySingleElement()
+    {
+        String key = "clear.element";
+        conf.clearProperty(key);
+        assertNull(key, conf.getProperty(key));
+    }
+
+    /**
+     * Tests that attributes are not effected if their parent element is
+     * cleared.
+     */
+    @Test
+    public void testClearPropertySingleElementWithAttribute()
+    {
+        String key = "clear.element2";
+        conf.clearProperty(key);
+        assertNull(key, conf.getProperty(key));
+        key = "clear.element2[@id]";
+        assertNotNull(key, conf.getProperty(key));
+    }
+
+    /**
+     * Tests whether a property with a comment can be cleared.
+     */
+    @Test
+    public void testClearPropertyCommentedElement()
+    {
+        String key = "clear.comment";
+        conf.clearProperty(key);
+        assertNull(key, conf.getProperty(key));
+    }
+
+    /**
+     * Tests whether a CDATA section can be cleared.
+     */
+    @Test
+    public void testClearPropertyCData()
+    {
+        String key = "clear.cdata";
+        conf.clearProperty(key);
+        assertNull(key, conf.getProperty(key));
+    }
+
+    /**
+     * Tests whether a list of elements can be cleared and whether attributes
+     * are retained.
+     */
+    @Test
+    public void testClearPropertyMultipleSiblings()
+    {
+        String key = "clear.list.item";
+        conf.clearProperty(key);
+        assertNull(key, conf.getProperty(key));
+        key = "clear.list.item[@id]";
+        assertNotNull(key, conf.getProperty(key));
+    }
+
+    /**
+     * Tests whether a list can be cleared that contains of multiple disjoint
+     * elements.
+     */
+    @Test
+    public void testClearPropertyMultipleDisjoint()
+    {
+        String key = "list.item";
+        conf.clearProperty(key);
+        assertNull(key, conf.getProperty(key));
+    }
+
+    /**
+     * Tests whether the text of the root element can be removed.
+     */
+    @Test
+    public void testClearTextRootElement() throws ConfigurationException,
+            IOException
+    {
+        final String xml = "<e a=\"v\">text</e>";
+        conf.clear();
+        StringReader in = new StringReader(xml);
+        source.load(in);
+        assertEquals("Wrong text of root", "text", conf.getString(null));
+        conf.clearProperty(null);
+        assertNull("Still got root text", conf.getString(null));
+        Configuration<ConfigurationNode> conf2 = reload();
+        assertNull("Got root text after reload", conf2.getString(null));
+    }
+
+    /**
+     * Tests whether an attribute can be replaced.
+     */
+    @Test
+    public void testSetAttributeExisting()
+    {
+        conf.setProperty("element3[@name]", "bar");
+        assertEquals("element3[@name]", "bar", conf
+                .getProperty("element3[@name]"));
+    }
+
+    /**
+     * Tests whether a new attribute can be set.
+     */
+    @Test
+    public void testSetAttributeNew()
+    {
+        conf.setProperty("foo[@bar]", "value");
+        assertEquals("foo[@bar]", "value", conf.getProperty("foo[@bar]"));
+    }
+
+    /**
+     * Tests whether an attribute of a different type than string can be set.
+     */
+    @Test
+    public void testAddObjectAttribute()
+    {
+        conf.addProperty("test.boolean[@value]", Boolean.TRUE);
+        assertTrue("test.boolean[@value]", conf
+                .getBoolean("test.boolean[@value]"));
+    }
+
+    /**
+     * Tests whether attributes on the root element can be set.
+     */
+    @Test
+    public void testSetRootAttribute() throws ConfigurationException
+    {
+        conf.setProperty("[@test]", "true");
+        assertEquals("Root attribute not set", "true", conf
+                .getString("[@test]"));
+        Configuration<ConfigurationNode> conf2 = reload();
+        assertTrue("Attribute not found after save", conf2
+                .containsKey("[@test]"));
+        conf2.setProperty("[@test]", "newValue");
+        assertEquals("New value not set", "newValue", conf2
+                .getString("[@test]"));
+        conf = conf2;
+        Configuration<ConfigurationNode> conf3 = reload();
+        assertEquals("Attribute not modified after save", "newValue", conf3
+                .getString("[@test]"));
+    }
+
+    /**
+     * Tests whether the text of the root element can be set.
+     */
+    @Test
+    public void testSetTextRootElement() throws ConfigurationException
+    {
+        final String text = "Root text";
+        conf.setProperty(null, text);
+        Configuration<ConfigurationNode> conf2 = reload();
+        assertEquals("Wrong root text (1)", text, conf2.getString(""));
+        assertEquals("Wrong root text (2)", text, conf2.getString(null));
+    }
+
+    /**
+     * Tests whether a property can be set.
+     */
+    @Test
+    public void testSetProperty()
+    {
+        conf.setProperty("element.string", "hello");
+        assertEquals("'element.string'", "hello", conf
+                .getString("element.string"));
+        assertEquals("XML value of element.string", "hello", conf
+                .getProperty("element.string"));
+    }
+
+    /**
+     * Tests whether a property can be added to an uninitialized configuration.
+     */
+    @Test
+    public void testAddProperty()
+    {
+        source.clear();
+        conf.addProperty("test.string", "hello");
+        assertEquals("'test.string'", "hello", conf.getString("test.string"));
+    }
+
+    /**
+     * Tests whether properties of other types than string can be added.
+     */
+    @Test
+    public void testAddObjectProperty()
+    {
+        conf.addProperty("test.boolean", Boolean.TRUE);
+        assertTrue("'test.boolean'", conf.getBoolean("test.boolean"));
+    }
+
+    /**
+     * Tests whether the configuration source's root node is initialized with a
+     * reference to the corresponding XML element.
+     */
+    @Test
+    public void testGetRootReference()
+    {
+        assertNotNull("Root node has no reference", source.getRootNode()
+                .getReference());
+    }
+
+    /**
+     * Tests whether a list property can be created by adding multiple values
+     * with the same key.
+     */
+    @Test
+    public void testAddList()
+    {
+        conf.addProperty("test.array", "value1");
+        conf.addProperty("test.array", "value2");
+
+        List<?> list = conf.getList("test.array");
+        assertNotNull("null list", list);
+        assertTrue("'value1' element missing", list.contains("value1"));
+        assertTrue("'value2' element missing", list.contains("value2"));
+        assertEquals("list size", 2, list.size());
+    }
+
+    /**
+     * Tries to load a non well-formed XML from a string.
+     */
+    @Test(expected = ConfigurationException.class)
+    public void testLoadInvalidXML() throws ConfigurationException, IOException
+    {
+        String xml = "<?xml version=\"1.0\"?><config><test>1</rest></config>";
+        source.load(new StringReader(xml));
+    }
+
+    /**
+     * Tests if a second file can be appended to a first.
+     */
+    @Test
+    public void testLoadAppend() throws ConfigurationException
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        Locator loc = new URLLocator(ConfigurationAssert
+                .getTestURL("testDigesterConfigurationInclude1.xml"));
+        locSupport.load(loc);
+        assertEquals("Property from file 1 not found", "value", conf
+                .getString("element"));
+        assertEquals("Property from file 2 not found", "tasks", conf
+                .getString("table.name"));
+        Configuration<ConfigurationNode> conf2 = reload();
+        assertEquals("Property from file 1 not found after reload", "value",
+                conf2.getString("element"));
+        assertEquals("Property from file 2 not found after reload", "tasks",
+                conf2.getString("table.name"));
+        assertEquals("Property not found", "application", conf2
+                .getString("table[@tableType]"));
+    }
+
+    /**
+     * Tests whether an invalid XML file can be loaded with the default
+     * (non-validating) document builder.
+     */
+    @Test
+    public void testLoadNonValidatingDocBuilder() throws ConfigurationException
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.load(new URLLocator(ConfigurationAssert
+                .getTestURL("testValidateInvalid.xml")));
+        assertEquals("key customers", "customers", conf.getString("table.name"));
+        assertFalse("key type", conf.containsKey("table.fields.field(1).type"));
+    }
+
+    /**
+     * Tries to load an invalid XML file with a custom, validating document
+     * builder. This should cause an exception.
+     */
+    @Test(expected = ConfigurationException.class)
+    public void testLoadValidatingDocBuilder() throws Exception
+    {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setValidating(true);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        builder.setErrorHandler(new DefaultHandler()
+        {
+            @Override
+            public void error(SAXParseException ex) throws SAXException
+            {
+                throw ex;
+            }
+        });
+        source.setDocumentBuilder(builder);
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.load(new URLLocator(ConfigurationAssert
+                .getTestURL("testValidateInvalid.xml")));
+    }
+
+    /**
+     * Tests whether a valid XML file can be loaded with a custom validating
+     * document builder.
+     */
+    @Test
+    public void testCustomDocBuilder() throws Exception
+    {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setValidating(true);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        builder.setErrorHandler(new DefaultHandler()
+        {
+            @Override
+            public void error(SAXParseException ex) throws SAXException
+            {
+                throw ex;
+            }
+        });
+        source.setDocumentBuilder(builder);
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.load(new URLLocator(ConfigurationAssert
+                .getTestURL("testValidateValid.xml")));
+        assertTrue("Key not found", conf
+                .containsKey("table.fields.field(1).type"));
+    }
+
+    /**
+     * Tests whether a DTD can be accessed.
+     */
+    @Test
+    public void testDtd() throws ConfigurationException
+    {
+        conf.clear();
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(new URLLocator(ConfigurationAssert
+                .getTestURL("testDtd.xml")));
+        locSupport.load();
+        assertEquals("Wrong value 1", "value1", conf.getString("entry(0)"));
+        assertEquals("Wrong value 2", "test2", conf.getString("entry(1)[@key]"));
+    }
+
+    /**
+     * Tests DTD validation using the setValidating() method if an invalid
+     * document is loaded and validation is switched off.
+     */
+    @Test
+    public void testValidatingFalse() throws ConfigurationException
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(new URLLocator(ConfigurationAssert
+                .getTestURL("testValidateInvalid.xml")));
+        assertFalse("Validating is true", source.isValidating());
+        locSupport.load();
+        assertEquals("Wrong value", "customers", conf.getString("table.name"));
+        assertFalse("Got type property", conf
+                .containsKey("table.fields.field(1).type"));
+    }
+
+    /**
+     * Tests DTD validation using the setValidating() method if an invalid
+     * document is loaded and validation is turned on.
+     */
+    @Test(expected = ConfigurationException.class)
+    public void testValidatingTrue() throws ConfigurationException
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(new URLLocator(ConfigurationAssert
+                .getTestURL("testValidateInvalid.xml")));
+        source.setValidating(true);
+        locSupport.load();
+    }
+
+    /**
+     * Tests whether attributes can be saved and loaded (related to issue
+     * 34442).
+     */
+    @Test
+    public void testSaveAttributes() throws Exception
+    {
+        conf.clear();
+        source.getCapability(LocatorSupport.class).load();
+        Configuration<ConfigurationNode> conf2 = reload();
+        assertEquals("Wrong attribute", "foo", conf2
+                .getString("element3[@name]"));
+    }
+
+    /**
+     * Tests whether the encoding is written to the generated XML file.
+     */
+    @Test
+    public void testSaveWithEncoding() throws ConfigurationException,
+            IOException
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setEncoding(ENCODING);
+        StringWriter out = new StringWriter();
+        source.save(out);
+        assertTrue("Encoding was not written to file", out.toString().indexOf(
+                "encoding=\"" + ENCODING + "\"") >= 0);
+    }
+
+    /**
+     * Tests whether a default encoding is used if no specific encoding is set.
+     * According to the XSLT specification (http://www.w3.org/TR/xslt#output)
+     * this should be either UTF-8 or UTF-16.
+     */
+    @Test
+    public void testSaveWithNullEncoding() throws ConfigurationException,
+            IOException
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setEncoding(null);
+        StringWriter out = new StringWriter();
+        source.save(out);
+        assertTrue("Encoding was written to file", out.toString().indexOf(
+                "encoding=\"UTF-") >= 0);
+    }
+
+    /**
+     * Tests whether the encoding is correctly detected by the XML parser. This
+     * is done by loading an XML file with the encoding "UTF-16". If this
+     * encoding is not detected correctly, an exception will be thrown that
+     * "Content is not allowed in prolog". This test case is related to issue
+     * 34204.
+     */
+    @Test
+    public void testLoadWithEncoding() throws ConfigurationException,
+            IOException
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setEncoding("UTF-16");
+        locSupport.setLocator(new URLLocator(ConfigurationAssert
+                .getTestURL("testEncoding.xml")));
+        locSupport.load();
+        assertEquals("Wrong value", "test3_yoge", conf.getString("yoge"));
+    }
+
+    /**
+     * Tests whether the DOCTYPE survives a save operation.
+     */
+    @Test
+    public void testSaveWithDoctype() throws ConfigurationException,
+            IOException
+    {
+        String content = "<?xml  version=\"1.0\"?>"
+                + DOCTYPE
+                + "properties"
+                + DOCTYPE_DECL
+                + "<properties version=\"1.0\"><entry key=\"test\">value</entry></properties>";
+        StringReader in = new StringReader(content);
+        source = new XMLConfigurationSource();
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(testFileLocator);
+        source.load(in);
+        assertEquals("Wrong public ID", PUBLIC_ID, source.getPublicID());
+        assertEquals("Wrong system ID", SYSTEM_ID, source.getSystemID());
+        StringWriter out = new StringWriter();
+        source.save(out);
+        assertTrue("Did not find DOCTYPE", out.toString().indexOf(DOCTYPE) >= 0);
+    }
+
+    /**
+     * Tests setting public and system IDs for the DOCTYPE and then saving the
+     * configuration source. This should generate a DOCTYPE declaration.
+     */
+    @Test
+    public void testSaveWithDoctypeIDs() throws ConfigurationException,
+            IOException
+    {
+        assertNull("A public ID was found", source.getPublicID());
+        assertNull("A system ID was found", source.getSystemID());
+        source.setPublicID(PUBLIC_ID);
+        source.setSystemID(SYSTEM_ID);
+        StringWriter out = new StringWriter();
+        source.save(out);
+        assertTrue("Did not find DOCTYPE", out.toString().indexOf(
+                DOCTYPE + "testconfig" + DOCTYPE_DECL) >= 0);
+    }
+
+    /**
+     * Tries to save a configuration source if an invalid transformer factory is
+     * specified. In this case the error thrown by the TransformerFactory class
+     * should be caught and re-thrown as a ConfigurationException.
+     */
+    @Test
+    public void testSaveWithInvalidTransformerFactory()
+            throws ConfigurationException, IOException
+    {
+        System.setProperty(PROP_FACTORY, "an.invalid.Class");
+        try
+        {
+            source.getCapability(LocatorSupport.class).save(testOutLocator);
+            fail("Could save with invalid TransformerFactory!");
+        }
+        catch (ConfigurationException cex)
+        {
+            // ok
+        }
+        finally
+        {
+            System.getProperties().remove(PROP_FACTORY);
+        }
+    }
+
+    /**
+     * Tests the mechanism for registering publicIds.
+     */
+    @Test
+    public void testRegisterEntityId() throws ConfigurationException,
+            IOException
+    {
+        URLLocator loc = new URLLocator(ConfigurationAssert
+                .getTestURL("testDtd.xml"));
+        URL dtdURL = ConfigurationAssert.getTestURL("properties.dtd");
+        final String publicId = "http://commons.apache.org/test/properties.dtd";
+        source = new XMLConfigurationSource();
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(loc);
+        locSupport.load();
+        source.setPublicID(publicId);
+        locSupport.save(testOutLocator);
+        XMLConfigurationSource source2 = new XMLConfigurationSource();
+        LocatorSupport locSupport2 = source2
+                .getCapability(LocatorSupport.class);
+        locSupport2.setLocator(testOutLocator);
+        source2.registerEntityId(publicId, dtdURL);
+        source2.setValidating(true);
+        locSupport2.load();
+    }
+
+    /**
+     * Tries to register a null public ID. This should cause an exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRegisterEntityIdNull() throws IOException
+    {
+        source.registerEntityId(null, new URL("http://commons.apache.org"));
+    }
+
+    /**
+     * Tests modifying an XML document and saving it with schema validation
+     * enabled.
+     */
+    @Test
+    public void testSaveWithValidation() throws ConfigurationException
+    {
+        CatalogResolver resolver = new CatalogResolver();
+        resolver.setCatalogFiles(CATALOG_FILES);
+        URLLocator loc = new URLLocator(ConfigurationAssert
+                .getTestURL("sample.xml"));
+        source = new XMLConfigurationSource();
+        source.setEntityResolver(resolver);
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(loc);
+        source.setSchemaValidation(true);
+        locSupport.load();
+        conf = new ConfigurationImpl<ConfigurationNode>(source);
+        conf.setProperty("Employee.SSN", "123456789");
+        source.validate();
+        Configuration<ConfigurationNode> conf2 = reload();
+        assertEquals("123456789", conf2.getString("Employee.SSN"));
+    }
+
+    /**
+     * Tests modifying an XML document and saving it with schema validation
+     * enabled.
+     */
+    @Test
+    public void testSaveWithValidationFailure() throws Exception
+    {
+        CatalogResolver resolver = new CatalogResolver();
+        resolver.setCatalogFiles(CATALOG_FILES);
+        URLLocator loc = new URLLocator(ConfigurationAssert
+                .getTestURL("sample.xml"));
+        source = new XMLConfigurationSource();
+        source.setEntityResolver(resolver);
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(loc);
+        source.setSchemaValidation(true);
+        locSupport.load();
+        conf = new ConfigurationImpl<ConfigurationNode>(source);
+        conf.setProperty("Employee.Email", "JohnDoe@apache.org");
+        try
+        {
+            source.validate();
+            fail("No validation failure on save");
+        }
+        catch (ConfigurationException e)
+        {
+            Throwable cause = e.getCause();
+            assertNotNull("No cause for exception on save", cause);
+            assertTrue("Incorrect exception on save",
+                    cause instanceof SAXParseException);
+        }
+    }
+
+    /**
+     * Tests whether spaces are preserved when the xml:space attribute is set.
+     */
+    @Test
+    public void testPreserveSpace()
+    {
+        assertEquals("Wrong value of blanc", " ", conf.getString("space.blanc"));
+        assertEquals("Wrong value of stars", " * * ", conf
+                .getString("space.stars"));
+    }
+
+    /**
+     * Tests whether the xml:space attribute can be overridden in nested
+     * elements.
+     */
+    @Test
+    public void testPreserveSpaceOverride()
+    {
+        assertEquals("Not trimmed", "Some text", conf
+                .getString("space.description"));
+    }
+
+    /**
+     * Tests an xml:space attribute with an invalid value. This will be
+     * interpreted as default.
+     */
+    @Test
+    public void testPreserveSpaceInvalid()
+    {
+        assertEquals("Invalid not trimmed", "Some other text", conf
+                .getString("space.testInvalid"));
+    }
+}

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestXMLConfigurationSource.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain