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 2014/03/01 21:13:55 UTC

svn commit: r1573219 - in /commons/proper/configuration/branches/immutableNodes/src: main/java/org/apache/commons/configuration/AbstractHierarchicalConfiguration.java test/java/org/apache/commons/configuration/TestAbstractHierarchicalConfiguration.java

Author: oheger
Date: Sat Mar  1 20:13:54 2014
New Revision: 1573219

URL: http://svn.apache.org/r1573219
Log:
Added new AbstractHierarchicalConfiguration base class.

This class provides basic functionality of a hierarchical configuration based
on a NodeModel. The idea is that by providing alternative implementations of
NodeModel, arbitrary hierarchical data structures can be integrated with this
class.

The current implementation is incomplete, there are still some failing tests.

Added:
    commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/AbstractHierarchicalConfiguration.java
    commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/TestAbstractHierarchicalConfiguration.java

Added: commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/AbstractHierarchicalConfiguration.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/AbstractHierarchicalConfiguration.java?rev=1573219&view=auto
==============================================================================
--- commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/AbstractHierarchicalConfiguration.java (added)
+++ commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/AbstractHierarchicalConfiguration.java Sat Mar  1 20:13:54 2014
@@ -0,0 +1,896 @@
+/*
+ * 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.configuration;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.ex.ConfigurationRuntimeException;
+import org.apache.commons.configuration.sync.LockMode;
+import org.apache.commons.configuration.sync.NoOpSynchronizer;
+import org.apache.commons.configuration.sync.Synchronizer;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.ConfigurationNodeVisitorAdapter;
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.configuration.tree.NodeAddData;
+import org.apache.commons.configuration.tree.NodeHandler;
+import org.apache.commons.configuration.tree.NodeKeyResolver;
+import org.apache.commons.configuration.tree.NodeModel;
+import org.apache.commons.configuration.tree.NodeTreeWalker;
+import org.apache.commons.configuration.tree.NodeUpdateData;
+import org.apache.commons.configuration.tree.QueryResult;
+
+/**
+ * <p>A specialized configuration class that extends its base class by the
+ * ability of keeping more structure in the stored properties.</p>
+ * <p>There
+ * are some sources of configuration data that cannot be stored very well in a
+ * {@code BaseConfiguration} object because then their structure is lost.
+ * This is for instance true for XML documents. This class can deal with such
+ * structured configuration sources by storing the properties in a tree-like
+ * organization. The exact storage structure of the underlying data does not
+ * matter for the configuration instance; it uses a {@link NodeModel} object
+ * for accessing it.</p>
+ * <p>The hierarchical organization allows for a more
+ * sophisticated access to single properties. As an example consider the
+ * following XML document:</p>
+ * <p>
+ *
+ * <pre>
+ * &lt;database&gt;
+ *   &lt;tables&gt;
+ *     &lt;table&gt;
+ *       &lt;name&gt;users&lt;/name&gt;
+ *       &lt;fields&gt;
+ *         &lt;field&gt;
+ *           &lt;name&gt;lid&lt;/name&gt;
+ *           &lt;type&gt;long&lt;/name&gt;
+ *         &lt;/field&gt;
+ *         &lt;field&gt;
+ *           &lt;name&gt;usrName&lt;/name&gt;
+ *           &lt;type&gt;java.lang.String&lt;/type&gt;
+ *         &lt;/field&gt;
+ *        ...
+ *       &lt;/fields&gt;
+ *     &lt;/table&gt;
+ *     &lt;table&gt;
+ *       &lt;name&gt;documents&lt;/name&gt;
+ *       &lt;fields&gt;
+ *         &lt;field&gt;
+ *           &lt;name&gt;docid&lt;/name&gt;
+ *           &lt;type&gt;long&lt;/type&gt;
+ *         &lt;/field&gt;
+ *         ...
+ *       &lt;/fields&gt;
+ *     &lt;/table&gt;
+ *     ...
+ *   &lt;/tables&gt;
+ * &lt;/database&gt;
+ * </pre>
+ *
+ * </p>
+ * <p>If this document is parsed and stored in a hierarchical configuration
+ * object (which can be done by one of
+ * the sub classes), there are enhanced possibilities of accessing properties.
+ * Per default, the keys for querying information can contain indices that select a specific
+ * element if there are multiple hits.</p>
+ * <p>For instance the key
+ * {@code tables.table(0).name} can be used to find out the name of the
+ * first table. In opposite {@code tables.table.name} would return a
+ * collection with the names of all available tables. Similarly the key
+ * {@code tables.table(1).fields.field.name} returns a collection with
+ * the names of all fields of the second table. If another index is added after
+ * the {@code field} element, a single field can be accessed:
+ * {@code tables.table(1).fields.field(0).name}.</p>
+ * <p>There is a
+ * {@code getMaxIndex()} method that returns the maximum allowed index
+ * that can be added to a given property key. This method can be used to iterate
+ * over all values defined for a certain property.</p>
+ * <p>Since the 1.3 release of <em>Commons Configuration</em> hierarchical
+ * configurations support an <em>expression engine</em>. This expression engine
+ * is responsible for evaluating the passed in configuration keys and map them
+ * to the stored properties. The examples above are valid for the default
+ * expression engine, which is used when a new {@code AbstractHierarchicalConfiguration}
+ * instance is created. With the {@code setExpressionEngine()} method a
+ * different expression engine can be set. For instance with
+ * {@link org.apache.commons.configuration.tree.xpath.XPathExpressionEngine}
+ * there is an expression engine available that supports configuration keys in
+ * XPATH syntax.</p>
+ * <p>In addition to the events common for all configuration classes hierarchical
+ * configurations support some more events that correspond to some specific
+ * methods and features:
+ * <dl><dt><em>EVENT_ADD_NODES</em></dt><dd>The {@code addNodes()} method
+ * was called; the event object contains the key, to which the nodes were added,
+ * and a collection with the new nodes as value.</dd>
+ * <dt><em>EVENT_CLEAR_TREE</em></dt><dd>The {@code clearTree()} method was
+ * called; the event object stores the key of the removed sub tree.</dd>
+ * <dt><em>EVENT_SUBNODE_CHANGED</em></dt><dd>A {@code SubnodeConfiguration}
+ * that was created from this configuration has been changed. The value property
+ * of the event object contains the original event object as it was sent by the
+ * subnode configuration.</dd></dl></p>
+ * <p>
+ * Whether an {@code AbstractHierarchicalConfiguration} object is thread-safe or not
+ * depends on the underlying {@code NodeModel} and the {@link Synchronizer} it is associated with.
+ * Some {@code NodeModel} implementations are inherently thread-safe; they do not require
+ * a special {@code Synchronizer}. (Per default, a
+ * dummy {@code Synchronizer} is used which is not thread-safe!) The methods
+ * for querying or updating configuration data invoke this {@code Synchronizer}
+ * accordingly. There is one exception to this rule: The {@link #getRootNode()}
+ * method is not guarded using the {@code Synchronizer}. This is due to the
+ * fact that the caller can do anything with this root node, so it is not
+ * clear which kind of synchronization should be performed. So when accessing
+ * the configuration's root node directly, the client application is responsible
+ * for proper synchronization. This is achieved by calling the methods
+ * {@link #lock(LockMode)}, and {@link #unlock(LockMode)} with a proper
+ * {@link LockMode} argument. In any case, it is recommended to not access the
+ * root node directly, but to use corresponding methods for querying or
+ * updating configuration data instead. Direct manipulations of a
+ * configuration's node structure circumvent many internal mechanisms and thus
+ * can cause undesired effects. For concrete subclasses dealing with specific
+ * node structures, this situation may be different.
+ * </p>
+ *
+ * @version $Id$
+ * @since 2.0
+ * @param  <T> the type of the nodes managed by this hierarchical configuration
+ */
+public abstract class AbstractHierarchicalConfiguration<T> extends AbstractConfiguration
+    implements Cloneable, NodeKeyResolver<T>
+{
+    /**
+     * Constant for the clear tree event.
+     * @since 1.3
+     */
+    public static final int EVENT_CLEAR_TREE = 10;
+
+    /**
+     * Constant for the add nodes event.
+     * @since 1.3
+     */
+    public static final int EVENT_ADD_NODES = 11;
+
+    /**
+     * Constant for the subnode configuration modified event.
+     * @since 1.5
+     */
+    public static final int EVENT_SUBNODE_CHANGED = 12;
+
+    /** The model for managing the data stored in this configuration. */
+    private final NodeModel<T> model;
+
+    /** Stores the expression engine for this instance.*/
+    private ExpressionEngine expressionEngine;
+
+    /**
+     * A map for managing the {@code SubnodeConfiguration} instances created
+     * from this configuration.
+     */
+    private Map<SubnodeConfiguration, Object> subConfigs;
+
+    /** A listener for reacting on changes to update sub configurations. */
+    private ConfigurationListener changeListener;
+
+    /**
+     * Creates a new instance of {@code AbstractHierarchicalConfiguration} and
+     * sets the {@code NodeModel} to be used.
+     *
+     * @param nodeModel the {@code NodeModel}
+     */
+    protected AbstractHierarchicalConfiguration(NodeModel<T> nodeModel)
+    {
+        model = nodeModel;
+    }
+
+    /**
+     * Returns the root node of this hierarchical configuration.
+     *
+     * @return the root node
+     */
+    public T getRootNode()
+    {
+        return getModel().getRootNode();
+    }
+
+    /**
+     * Sets the root node of this hierarchical configuration.
+     *
+     * @param rootNode the root node
+     */
+    public final void setRootNode(T rootNode)
+    {
+        if (rootNode == null)
+        {
+            throw new IllegalArgumentException("Root node must not be null!");
+        }
+
+        beginWrite(false);
+        try
+        {
+            setRootNodeInternal(rootNode);
+        }
+        finally
+        {
+            endWrite();
+        }
+    }
+
+    /**
+     * Actually sets the root node of this configuration. This method is called
+     * by {@code setRootNode()}. Subclasses that need to adapt this operation
+     * can override it.
+     *
+     * @param rootNode the new root node of this configuration
+     */
+    protected void setRootNodeInternal(T rootNode)
+    {
+        getModel().setRootNode(rootNode);
+    }
+
+    /**
+     * {@inheritDoc} This implementation handles synchronization and delegates
+     * to {@code getRootElementNameInternal()}.
+     */
+    public final String getRootElementName()
+    {
+        beginRead(false);
+        try
+        {
+            return getRootElementNameInternal();
+        }
+        finally
+        {
+            endRead();
+        }
+    }
+
+    /**
+     * Actually obtains the name of the root element. This method is called by
+     * {@code getRootElementName()}. It just returns the name of the root node.
+     * Subclasses that treat the root element name differently can override this
+     * method.
+     *
+     * @return the name of this configuration's root element
+     */
+    protected String getRootElementNameInternal()
+    {
+        return getModel().getNodeHandler().nodeName(getRootNode());
+    }
+
+    /**
+     * Returns the expression engine used by this configuration. This method
+     * will never return <b>null</b>; if no specific expression engine was set,
+     * the default expression engine will be returned.
+     *
+     * @return the current expression engine
+     * @since 1.3
+     */
+    public ExpressionEngine getExpressionEngine()
+    {
+        return (expressionEngine != null) ? expressionEngine
+                : DefaultExpressionEngine.INSTANCE;
+    }
+
+    /**
+     * Sets the expression engine to be used by this configuration. All property
+     * keys this configuration has to deal with will be interpreted by this
+     * engine.
+     *
+     * @param expressionEngine the new expression engine; can be <b>null</b>,
+     * then the default expression engine will be used
+     * @since 1.3
+     */
+    public void setExpressionEngine(ExpressionEngine expressionEngine)
+    {
+        this.expressionEngine = expressionEngine;
+    }
+
+    /**
+     * Fetches the specified property. This task is delegated to the associated
+     * expression engine.
+     *
+     * @param key the key to be looked up
+     * @return the found value
+     */
+    @Override
+    protected Object getPropertyInternal(String key)
+    {
+        List<QueryResult<T>> results = fetchNodeList(key);
+
+        if (results.isEmpty())
+        {
+            return null;
+        }
+        else
+        {
+            NodeHandler<T> handler = getModel().getNodeHandler();
+            List<Object> list = new ArrayList<Object>();
+            for (QueryResult<T> result : results)
+            {
+                Object value = valueFromResult(result, handler);
+                if (value != null)
+                {
+                    list.add(value);
+                }
+            }
+
+            if (list.size() < 1)
+            {
+                return null;
+            }
+            else
+            {
+                return (list.size() == 1) ? list.get(0) : list;
+            }
+        }
+    }
+
+    /**
+     * Adds the property with the specified key. This task will be delegated to
+     * the associated {@code ExpressionEngine}, so the passed in key
+     * must match the requirements of this implementation.
+     *
+     * @param key the key of the new property
+     * @param obj the value of the new property
+     */
+    @Override
+    protected void addPropertyDirect(String key, Object obj)
+    {
+        getModel().addProperty(key, getListDelimiterHandler().parse(obj), this);
+    }
+
+    /**
+     * Adds a collection of nodes at the specified position of the configuration
+     * tree. This method works similar to {@code addProperty()}, but
+     * instead of a single property a whole collection of nodes can be added -
+     * and thus complete configuration sub trees. E.g. with this method it is
+     * possible to add parts of another {@code BaseHierarchicalConfiguration}
+     * object to this object. (However be aware that a
+     * {@code ConfigurationNode} object can only belong to a single
+     * configuration. So if nodes from one configuration are directly added to
+     * another one using this method, the structure of the source configuration
+     * will be broken. In this case you should clone the nodes to be added
+     * before calling {@code addNodes()}.) If the passed in key refers to
+     * an existing and unique node, the new nodes are added to this node.
+     * Otherwise a new node will be created at the specified position in the
+     * hierarchy. Implementation node: This method performs some book-keeping
+     * and then delegates to {@code addNodesInternal()}.
+     *
+     * @param key the key where the nodes are to be added; can be <b>null</b>,
+     * then they are added to the root node
+     * @param nodes a collection with the {@code Node} objects to be
+     * added
+     */
+    public final void addNodes(String key, Collection<? extends T> nodes)
+    {
+        if (nodes == null || nodes.isEmpty())
+        {
+            return;
+        }
+
+        beginWrite(false);
+        try
+        {
+            fireEvent(EVENT_ADD_NODES, key, nodes, true);
+            addNodesInternal(key, nodes);
+            fireEvent(EVENT_ADD_NODES, key, nodes, false);
+        }
+        finally
+        {
+            endWrite();
+        }
+    }
+
+    /**
+     * Actually adds a collection of new nodes to this configuration. This
+     * method is called by {@code addNodes()}. It can be overridden by
+     * subclasses that need to adapt this operation.
+     *
+     * @param key the key where the nodes are to be added; can be <b>null</b>,
+     *        then they are added to the root node
+     * @param nodes a collection with the {@code Node} objects to be added
+     * @since 2.0
+     */
+    protected void addNodesInternal(String key, Collection<? extends T> nodes)
+    {
+        getModel().addNodes(key, nodes, this);
+    }
+
+    /**
+     * Checks if this configuration is empty. Empty means that there are no keys
+     * with any values, though there can be some (empty) nodes.
+     *
+     * @return a flag if this configuration is empty
+     */
+    @Override
+    protected boolean isEmptyInternal()
+    {
+        return !nodeDefined(getRootNode());
+    }
+
+    /**
+     * Checks if the specified key is contained in this configuration. Note that
+     * for this configuration the term &quot;contained&quot; means that the key
+     * has an associated value. If there is a node for this key that has no
+     * value but children (either defined or undefined), this method will still
+     * return <b>false </b>.
+     *
+     * @param key the key to be checked
+     * @return a flag if this key is contained in this configuration
+     */
+    @Override
+    protected boolean containsKeyInternal(String key)
+    {
+        return getPropertyInternal(key) != null;
+    }
+
+    /**
+     * Sets the value of the specified property.
+     *
+     * @param key the key of the property to set
+     * @param value the new value of this property
+     */
+    @Override
+    protected void setPropertyInternal(String key, Object value)
+    {
+        getModel().setProperty(key, value, this);
+    }
+
+    /**
+     * {@inheritDoc} This implementation delegates to the expression engine.
+     */
+    public List<QueryResult<T>> resolveKey(T root, String key,
+            NodeHandler<T> handler)
+    {
+        return getExpressionEngine().query(root, key, handler);
+    }
+
+    /**
+     * {@inheritDoc} This implementation delegates to the expression engine.
+     */
+    public NodeAddData<T> resolveAddKey(T root, String key,
+            NodeHandler<T> handler)
+    {
+        return getExpressionEngine().prepareAdd(root, key, handler);
+    }
+
+    /**
+     * {@inheritDoc} This implementation executes a query for the given key and
+     * constructs a {@code NodeUpdateData} object based on the results. It
+     * determines which nodes need to be changed and whether new ones need to be
+     * added or existing ones need to be removed.
+     */
+    public NodeUpdateData<T> resolveUpdateKey(T root, String key,
+            Object newValue, NodeHandler<T> handler)
+    {
+        Iterator<QueryResult<T>> itNodes = fetchNodeList(key).iterator();
+        Iterator<?> itValues = getListDelimiterHandler().parse(newValue).iterator();
+        Map<QueryResult<T>, Object> changedValues =
+                new HashMap<QueryResult<T>, Object>();
+        Collection<Object> additionalValues = null;
+        Collection<QueryResult<T>> removedItems = null;
+
+        while (itNodes.hasNext() && itValues.hasNext())
+        {
+            changedValues.put(itNodes.next(), itValues.next());
+        }
+
+        // Add additional nodes if necessary
+        if (itValues.hasNext())
+        {
+            additionalValues = new LinkedList<Object>();
+            while (itValues.hasNext())
+            {
+                additionalValues.add(itValues.next());
+            }
+        }
+
+        // Remove remaining nodes
+        if (itNodes.hasNext())
+        {
+            removedItems = new LinkedList<QueryResult<T>>();
+            while (itNodes.hasNext())
+            {
+                removedItems.add(itNodes.next());
+            }
+        }
+
+        return new NodeUpdateData<T>(changedValues, additionalValues,
+                removedItems, key);
+    }
+
+    /**
+     * Clears this configuration. This is a more efficient implementation than
+     * the one inherited from the base class. It delegates to the node model.
+     */
+    @Override
+    protected void clearInternal()
+    {
+        getModel().clear();
+    }
+
+    /**
+     * Removes all values of the property with the given name and of keys that
+     * start with this name. So if there is a property with the key
+     * &quot;foo&quot; and a property with the key &quot;foo.bar&quot;, a call
+     * of {@code clearTree("foo")} would remove both properties.
+     *
+     * @param key the key of the property to be removed
+     */
+    public final void clearTree(String key)
+    {
+        beginWrite(false);
+        try
+        {
+            fireEvent(EVENT_CLEAR_TREE, key, null, true);
+            List<ConfigurationNode> nodes = clearTreeInternal(key);
+            fireEvent(EVENT_CLEAR_TREE, key, nodes, false);
+        }
+        finally
+        {
+            endWrite();
+        }
+    }
+
+    /**
+     * Actually clears the tree of elements referenced by the given key. This
+     * method is called by {@code clearTree()}. Subclasses that need to adapt
+     * this operation can override this method. This base implementation
+     * delegates to the node model.
+     *
+     * @param key the key of the property to be removed
+     * @return a collection with the nodes that have been removed (this is
+     *         needed for firing a meaningful event of type EVENT_CLEAR_TREE)
+     * @since 2.0
+     */
+    protected List<ConfigurationNode> clearTreeInternal(String key)
+    {
+        getModel().clearTree(key, this);
+        //TODO return something meaningful
+        return null;
+    }
+
+    /**
+     * Removes the property with the given key. Properties with names that start
+     * with the given key (i.e. properties below the specified key in the
+     * hierarchy) won't be affected. This implementation delegates to the node+
+     * model.
+     *
+     * @param key the key of the property to be removed
+     */
+    @Override
+    protected void clearPropertyDirect(String key)
+    {
+        getModel().clearProperty(key, this);
+    }
+
+    /**
+     * Returns an iterator with all keys defined in this configuration.
+     * Note that the keys returned by this method will not contain any
+     * indices. This means that some structure will be lost.</p>
+     *
+     * @return an iterator with the defined keys in this configuration
+     */
+    @Override
+    protected Iterator<String> getKeysInternal()
+    {
+        DefinedKeysVisitor<T> visitor = new DefinedKeysVisitor<T>();
+        NodeTreeWalker.INSTANCE.walkDFS(getModel().getRootNode(), visitor,
+                getModel().getNodeHandler());
+
+        return visitor.getKeyList().iterator();
+    }
+
+    /**
+     * Returns an iterator with all keys defined in this configuration that
+     * start with the given prefix. The returned keys will not contain any
+     * indices. This implementation tries to locate a node whose key is the same
+     * as the passed in prefix. Then the subtree of this node is traversed, and
+     * the keys of all nodes encountered (including attributes) are added to the
+     * result set.
+     *
+     * @param prefix the prefix of the keys to start with
+     * @return an iterator with the found keys
+     */
+    @Override
+    protected Iterator<String> getKeysInternal(String prefix)
+    {
+        DefinedKeysVisitor<T> visitor = new DefinedKeysVisitor<T>(prefix);
+        if (containsKey(prefix))
+        {
+            // explicitly add the prefix
+            visitor.getKeyList().add(prefix);
+        }
+
+        List<QueryResult<T>> nodes = fetchNodeList(prefix);
+        NodeHandler<T> handler = getModel().getNodeHandler();
+
+        for (QueryResult<T> node : nodes)
+        {
+            //TODO handle attributes
+            for (T c : handler.getChildren(node.getNode()))
+            {
+                NodeTreeWalker.INSTANCE.walkDFS(c, visitor, handler);
+            }
+        }
+
+        return visitor.getKeyList().iterator();
+    }
+
+    /**
+     * Returns the maximum defined index for the given key. This is useful if
+     * there are multiple values for this key. They can then be addressed
+     * separately by specifying indices from 0 to the return value of this
+     * method. If the passed in key is not contained in this configuration,
+     * result is -1.
+     *
+     * @param key the key to be checked
+     * @return the maximum defined index for this key
+     */
+    public final int getMaxIndex(String key)
+    {
+        beginRead(false);
+        try
+        {
+            return getMaxIndexInternal(key);
+        }
+        finally
+        {
+            endRead();
+        }
+    }
+
+    /**
+     * Actually retrieves the maximum defined index for the given key. This
+     * method is called by {@code getMaxIndex()}. Subclasses that need to adapt
+     * this operation have to override this method.
+     *
+     * @param key the key to be checked
+     * @return the maximum defined index for this key
+     * @since 2.0
+     */
+    protected int getMaxIndexInternal(String key)
+    {
+        return fetchNodeList(key).size() - 1;
+    }
+
+    /**
+     * Creates a copy of this object. This new configuration object will contain
+     * copies of all nodes in the same structure. Registered event listeners
+     * won't be cloned; so they are not registered at the returned copy.
+     *
+     * @return the copy
+     * @since 1.2
+     */
+    @Override
+    public Object clone()
+    {
+        beginRead(false);
+        try
+        {
+            AbstractHierarchicalConfiguration copy = (AbstractHierarchicalConfiguration) super
+                    .clone();
+            copy.setSynchronizer(NoOpSynchronizer.INSTANCE);
+            copy.cloneInterpolator(this);
+            copy.setSynchronizer(ConfigurationUtils.cloneSynchronizer(getSynchronizer()));
+
+            //TODO clone node model
+
+            return copy;
+        }
+        catch (CloneNotSupportedException cex)
+        {
+            // should not happen
+            throw new ConfigurationRuntimeException(cex);
+        }
+        finally
+        {
+            endRead();
+        }
+    }
+
+    /**
+     * Creates a clone of the node model. This method is called by
+     * {@code clone()}.
+     *
+     * @return the clone of the {@code NodeModel}
+     * @since 2.0
+     */
+    protected abstract NodeModel<T> cloneNodeModel();
+
+    /**
+     * Helper method for resolving the specified key.
+     *
+     * @param key the key
+     * @return a list with all results selected by this key
+     */
+    protected List<QueryResult<T>> fetchNodeList(String key)
+    {
+        return getExpressionEngine().query(getRootNode(), key,
+                getModel().getNodeHandler());
+    }
+
+    /**
+     * Checks if the specified node is defined.
+     *
+     * @param node the node to be checked
+     * @return a flag if this node is defined
+     */
+    protected boolean nodeDefined(T node)
+    {
+        DefinedVisitor<T> visitor = new DefinedVisitor<T>();
+        NodeTreeWalker.INSTANCE.walkBFS(node, visitor, getModel().getNodeHandler());
+        return visitor.isDefined();
+    }
+
+    /**
+     * Returns the model used by this configuration.
+     *
+     * @return the node model
+     */
+    public NodeModel<T> getModel()
+    {
+        return model;
+    }
+
+    /**
+     * Extracts the value from a query result.
+     *
+     * @param result the {@code QueryResult}
+     * @param handler the {@code NodeHandler}
+     * @return the value of this result (may be <b>null</b>)
+     */
+    private Object valueFromResult(QueryResult<T> result, NodeHandler<T> handler)
+    {
+        return result.isAttributeResult() ? result.getAttributeValue(handler)
+                : handler.getValue(result.getNode());
+    }
+
+    /**
+     * A specialized visitor that checks if a node is defined.
+     * &quot;Defined&quot; in this terms means that the node or at least one of
+     * its sub nodes is associated with a value.
+     *
+     */
+    private static class DefinedVisitor<T> extends
+            ConfigurationNodeVisitorAdapter<T>
+    {
+        /** Stores the defined flag. */
+        private boolean defined;
+
+        /**
+         * Checks if iteration should be stopped. This can be done if the first
+         * defined node is found.
+         *
+         * @return a flag if iteration should be stopped
+         */
+        @Override
+        public boolean terminate()
+        {
+            return isDefined();
+        }
+
+        /**
+         * Visits the node. Checks if a value is defined.
+         *
+         * @param node the actual node
+         */
+        @Override
+        public void visitBeforeChildren(T node, NodeHandler<T> handler)
+        {
+            defined =
+                    handler.getValue(node) != null
+                            || !handler.getAttributes(node).isEmpty();
+        }
+
+        /**
+         * Returns the defined flag.
+         *
+         * @return the defined flag
+         */
+        public boolean isDefined()
+        {
+            return defined;
+        }
+    }
+
+    /**
+     * A specialized visitor that fills a list with keys that are defined in a
+     * node hierarchy.
+     *
+     * @param <T> the type of the nodes to be visited
+     */
+    private class DefinedKeysVisitor<T> extends
+            ConfigurationNodeVisitorAdapter<T>
+    {
+        /** Stores the list to be filled. */
+        private final Set<String> keyList;
+
+        /** A stack with the keys of the already processed nodes. */
+        private final Stack<String> parentKeys;
+
+        /**
+         * Default constructor.
+         */
+        public DefinedKeysVisitor()
+        {
+            keyList = new LinkedHashSet<String>();
+            parentKeys = new Stack<String>();
+        }
+
+        /**
+         * Creates a new {@code DefinedKeysVisitor} instance and sets the
+         * prefix for the keys to fetch.
+         *
+         * @param prefix the prefix
+         */
+        public DefinedKeysVisitor(String prefix)
+        {
+            this();
+            parentKeys.push(prefix);
+        }
+
+        /**
+         * Returns the list with all defined keys.
+         *
+         * @return the list with the defined keys
+         */
+        public Set<String> getKeyList()
+        {
+            return keyList;
+        }
+
+        /**
+         * {@inheritDoc} This implementation removes this
+         * node's key from the stack.
+         */
+        @Override
+        public void visitAfterChildren(T node, NodeHandler<T> handler)
+        {
+            parentKeys.pop();
+        }
+
+        /**
+         * {@inheritDoc} If this node has a value, its key is added
+         * to the internal list.
+         *
+         * TODO handle attributes
+         */
+        @Override
+        public void visitBeforeChildren(T node, NodeHandler<T> handler)
+        {
+            String parentKey = parentKeys.isEmpty() ? null
+                    : (String) parentKeys.peek();
+            String key = getExpressionEngine().nodeKey(node, parentKey, handler);
+            parentKeys.push(key);
+            if (handler.getValue(node) != null)
+            {
+                keyList.add(key);
+            }
+        }
+    }
+}

Added: commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/TestAbstractHierarchicalConfiguration.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/TestAbstractHierarchicalConfiguration.java?rev=1573219&view=auto
==============================================================================
--- commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/TestAbstractHierarchicalConfiguration.java (added)
+++ commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/TestAbstractHierarchicalConfiguration.java Sat Mar  1 20:13:54 2014
@@ -0,0 +1,888 @@
+/*
+ * 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.configuration;
+
+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.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.configuration.convert.DefaultListDelimiterHandler;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.tree.DefaultConfigurationKey;
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.DefaultExpressionEngineSymbols;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.configuration.tree.ImmutableNode;
+import org.apache.commons.configuration.tree.InMemoryNodeModel;
+import org.apache.commons.configuration.tree.NodeModel;
+import org.apache.commons.configuration.tree.NodeStructureHelper;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Test class for {@code AbstractHierarchicalConfiguration}.
+ *
+ * @version $Id$
+ */
+public class TestAbstractHierarchicalConfiguration
+{
+    /** The test configuration. */
+    private AbstractHierarchicalConfiguration<ImmutableNode> config;
+
+    @Before
+    public void setUp() throws Exception
+    {
+        ImmutableNode root =
+                new ImmutableNode.Builder(1).addChild(
+                        NodeStructureHelper.ROOT_TABLES_TREE).create();
+        config =
+                new AbstractHierarchicalConfigurationTestImpl(
+                        new InMemoryNodeModel(root));
+    }
+
+    @Test
+    public void testSetRootNode()
+    {
+        ImmutableNode root = NodeStructureHelper.createNode("testNode", null);
+        config.setRootNode(root);
+        assertSame("Wrong root node", root, config.getRootNode());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testSetRootNodeNull()
+    {
+        config.setRootNode(null);
+    }
+
+    @Test
+    public void testIsEmptyFalse()
+    {
+        assertFalse(config.isEmpty());
+    }
+
+    /**
+     * Tests isEmpty() if only the root node exists.
+     */
+    @Test
+    public void testIsEmptyRootOnly()
+    {
+        config =
+                new AbstractHierarchicalConfigurationTestImpl(
+                        new InMemoryNodeModel());
+        assertTrue("Not empty", config.isEmpty());
+    }
+
+    /**
+     * Tests isEmpty() if the structure contains some nodes without values.
+     */
+    @Test
+    public void testIsEmptyNodesWithNoValues()
+    {
+        ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(1);
+        ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(1);
+        nodeBuilder.addChild(NodeStructureHelper.createNode("child", null));
+        rootBuilder.addChild(nodeBuilder.create());
+        config =
+                new AbstractHierarchicalConfigurationTestImpl(
+                        new InMemoryNodeModel(rootBuilder.create()));
+        assertTrue("Not empty", config.isEmpty());
+    }
+
+    private static void checkGetProperty(AbstractHierarchicalConfiguration<?> testConfig)
+    {
+        assertNull(testConfig.getProperty("tables.table.resultset"));
+        assertNull(testConfig.getProperty("tables.table.fields.field"));
+
+        Object prop = testConfig.getProperty("tables.table(0).fields.field.name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(5, ((Collection<?>) prop).size());
+
+        prop = testConfig.getProperty("tables.table.fields.field.name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(10, ((Collection<?>) prop).size());
+
+        prop = testConfig.getProperty("tables.table.fields.field(3).name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(2, ((Collection<?>) prop).size());
+
+        prop = testConfig.getProperty("tables.table(1).fields.field(2).name");
+        assertNotNull(prop);
+        assertEquals("creationDate", prop.toString());
+    }
+
+    @Test
+    public void testGetProperty()
+    {
+        checkGetProperty(config);
+    }
+
+    @Test
+    public void testSetProperty()
+    {
+        config.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
+        config.setProperty("tables.table(0).name", "resources");
+        assertEquals("resources", config.getString("tables.table(0).name"));
+        config.setProperty("tables.table.name", "tab1,tab2");
+        assertEquals("tab1", config.getString("tables.table(0).name"));
+        assertEquals("tab2", config.getString("tables.table(1).name"));
+
+        config.setProperty("test.items.item", new int[] { 2, 4, 8, 16 });
+        assertEquals(3, config.getMaxIndex("test.items.item"));
+        assertEquals(8, config.getInt("test.items.item(2)"));
+        config.setProperty("test.items.item(2)", new Integer(6));
+        assertEquals(6, config.getInt("test.items.item(2)"));
+        config.setProperty("test.items.item(2)", new int[] { 7, 9, 11 });
+        assertEquals(5, config.getMaxIndex("test.items.item"));
+
+        config.setProperty("test", Boolean.TRUE);
+        config.setProperty("test.items", "01/01/05");
+        assertEquals(5, config.getMaxIndex("test.items.item"));
+        assertTrue(config.getBoolean("test"));
+        assertEquals("01/01/05", config.getProperty("test.items"));
+
+        config.setProperty("test.items.item", new Integer(42));
+        assertEquals(0, config.getMaxIndex("test.items.item"));
+        assertEquals(42, config.getInt("test.items.item"));
+    }
+
+    @Test
+    public void testClear()
+    {
+        config.setProperty(null, "value");
+        config.addProperty("[@attr]", "defined");
+        config.clear();
+        assertTrue("Configuration not empty", config.isEmpty());
+    }
+
+    @Test
+    public void testClearProperty()
+    {
+        config.clearProperty("tables.table(0).fields.field(0).name");
+        assertEquals("uname", config.getProperty("tables.table(0).fields.field(0).name"));
+        config.clearProperty("tables.table(0).name");
+        assertFalse(config.containsKey("tables.table(0).name"));
+        assertEquals("firstName", config.getProperty("tables.table(0).fields.field(1).name"));
+        assertEquals("documents", config.getProperty("tables.table.name"));
+        config.clearProperty("tables.table");
+        assertEquals("documents", config.getProperty("tables.table.name"));
+
+        config.addProperty("test", "first");
+        config.addProperty("test.level", "second");
+        config.clearProperty("test");
+        assertEquals("second", config.getString("test.level"));
+        assertFalse(config.containsKey("test"));
+    }
+
+    @Test
+    public void testClearTree()
+    {
+        Object prop = config.getProperty("tables.table(0).fields.field.name");
+        assertNotNull(prop);
+        config.clearTree("tables.table(0).fields.field(3)");
+        prop = config.getProperty("tables.table(0).fields.field.name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(4, ((Collection<?>) prop).size());
+
+        config.clearTree("tables.table(0).fields");
+        assertNull(config.getProperty("tables.table(0).fields.field.name"));
+        prop = config.getProperty("tables.table.fields.field.name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(5, ((Collection<?>) prop).size());
+
+        config.clearTree("tables.table(1)");
+        assertNull(config.getProperty("tables.table.fields.field.name"));
+    }
+
+    /**
+     * Tests removing more complex node structures.
+     */
+    @Test
+    public void testClearTreeComplex()
+    {
+        final int count = 5;
+        // create the structure
+        for (int idx = 0; idx < count; idx++)
+        {
+            config.addProperty("indexList.index(-1)[@default]", Boolean.FALSE);
+            config.addProperty("indexList.index[@name]", "test" + idx);
+            config.addProperty("indexList.index.dir", "testDir" + idx);
+        }
+        assertEquals("Wrong number of nodes", count - 1, config
+                .getMaxIndex("indexList.index[@name]"));
+
+        // Remove a sub tree
+        boolean found = false;
+        for (int idx = 0; true; idx++)
+        {
+            String name = config.getString("indexList.index(" + idx
+                    + ")[@name]");
+            if (name == null)
+            {
+                break;
+            }
+            if ("test3".equals(name))
+            {
+                assertEquals("Wrong dir", "testDir3", config
+                        .getString("indexList.index(" + idx + ").dir"));
+                config.clearTree("indexList.index(" + idx + ")");
+                found = true;
+            }
+        }
+        assertTrue("Key to remove not found", found);
+        assertEquals("Wrong number of nodes after remove", count - 2, config
+                .getMaxIndex("indexList.index[@name]"));
+        assertEquals("Wrong number of dir nodes after remove", count - 2,
+                config.getMaxIndex("indexList.index.dir"));
+
+        // Verify
+        for (int idx = 0; true; idx++)
+        {
+            String name = config.getString("indexList.index(" + idx
+                    + ")[@name]");
+            if (name == null)
+            {
+                break;
+            }
+            if ("test3".equals(name))
+            {
+                fail("Key was not removed!");
+            }
+        }
+    }
+
+    /**
+     * Tests the clearTree() method on a hierarchical structure of nodes. This
+     * is a test case for CONFIGURATION-293.
+     */
+    @Test
+    public void testClearTreeHierarchy()
+    {
+        config.addProperty("a.b.c", "c");
+        config.addProperty("a.b.c.d", "d");
+        config.addProperty("a.b.c.d.e", "e");
+        config.clearTree("a.b.c");
+        assertFalse("Property not removed", config.containsKey("a.b.c"));
+        assertFalse("Sub property not removed", config.containsKey("a.b.c.d"));
+    }
+
+    @Test
+    public void testContainsKey()
+    {
+        assertTrue(config.containsKey("tables.table(0).name"));
+        assertTrue(config.containsKey("tables.table(1).name"));
+        assertFalse(config.containsKey("tables.table(2).name"));
+
+        assertTrue(config.containsKey("tables.table(0).fields.field.name"));
+        assertFalse(config.containsKey("tables.table(0).fields.field"));
+        config.clearTree("tables.table(0).fields");
+        assertFalse(config.containsKey("tables.table(0).fields.field.name"));
+
+        assertTrue(config.containsKey("tables.table.fields.field.name"));
+    }
+
+    @Test
+    public void testGetKeys()
+    {
+        List<String> keys = new ArrayList<String>();
+        for (Iterator<String> it = config.getKeys(); it.hasNext();)
+        {
+            keys.add(it.next());
+        }
+
+        assertEquals(2, keys.size());
+        assertTrue(keys.contains("tables.table.name"));
+        assertTrue(keys.contains("tables.table.fields.field.name"));
+
+        // test the order of the keys returned
+        config.addProperty("order.key1", "value1");
+        config.addProperty("order.key2", "value2");
+        config.addProperty("order.key3", "value3");
+
+        Iterator<String> it = config.getKeys("order");
+        assertEquals("1st key", "order.key1", it.next());
+        assertEquals("2nd key", "order.key2", it.next());
+        assertEquals("3rd key", "order.key3", it.next());
+    }
+
+    @Test
+    public void testGetKeysString()
+    {
+        // add some more properties to make it more interesting
+        config.addProperty("tables.table(0).fields.field(1).type", "VARCHAR");
+        config.addProperty("tables.table(0)[@type]", "system");
+        config.addProperty("tables.table(0).size", "42");
+        config.addProperty("tables.table(0).fields.field(0).size", "128");
+        config.addProperty("connections.connection.param.url", "url1");
+        config.addProperty("connections.connection.param.user", "me");
+        config.addProperty("connections.connection.param.pwd", "secret");
+        config.addProperty("connections.connection(-1).param.url", "url2");
+        config.addProperty("connections.connection(1).param.user", "guest");
+
+        checkKeys("tables.table(1)", new String[] { "name", "fields.field.name" });
+        checkKeys("tables.table(0)",
+                new String[]{"name", "fields.field.name", "tables.table(0)[@type]", "size", "fields.field.type", "fields.field.size"});
+        checkKeys("connections.connection(0).param",
+                new String[]{"url", "user", "pwd"});
+        checkKeys("connections.connection(1).param",
+                new String[]{"url", "user"});
+    }
+
+    /**
+     * Tests getKeys() with a prefix when the prefix matches exactly a key.
+     */
+    @Test
+    public void testGetKeysWithKeyAsPrefix()
+    {
+        config.addProperty("order.key1", "value1");
+        config.addProperty("order.key2", "value2");
+        Iterator<String> it = config.getKeys("order.key1");
+        assertTrue("no key found", it.hasNext());
+        assertEquals("1st key", "order.key1", it.next());
+        assertFalse("more keys than expected", it.hasNext());
+    }
+
+    /**
+     * Tests getKeys() with a prefix when the prefix matches exactly a key, and
+     * there are multiple keys starting with this prefix.
+     */
+    @Test
+    public void testGetKeysWithKeyAsPrefixMultiple()
+    {
+        config.addProperty("order.key1", "value1");
+        config.addProperty("order.key1.test", "value2");
+        config.addProperty("order.key1.test.complex", "value2");
+        Iterator<String> it = config.getKeys("order.key1");
+        assertEquals("Wrong key 1", "order.key1", it.next());
+        assertEquals("Wrong key 2", "order.key1.test", it.next());
+        assertEquals("Wrong key 3", "order.key1.test.complex", it.next());
+        assertFalse("More keys than expected", it.hasNext());
+    }
+
+    @Test
+    public void testAddProperty()
+    {
+        config.addProperty("tables.table(0).fields.field(-1).name", "phone");
+        Object prop = config.getProperty("tables.table(0).fields.field.name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(6, ((Collection<?>) prop).size());
+
+        config.addProperty("tables.table(0).fields.field.name", "fax");
+        prop = config.getProperty("tables.table.fields.field(5).name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof List);
+        List<?> list = (List<?>) prop;
+        assertEquals("phone", list.get(0));
+        assertEquals("fax", list.get(1));
+
+        config.addProperty("tables.table(-1).name", "config");
+        prop = config.getProperty("tables.table.name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(3, ((Collection<?>) prop).size());
+        config.addProperty("tables.table(2).fields.field(0).name", "cid");
+        config.addProperty("tables.table(2).fields.field(-1).name",
+        "confName");
+        prop = config.getProperty("tables.table(2).fields.field.name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(2, ((Collection<?>) prop).size());
+        assertEquals("confName",
+        config.getProperty("tables.table(2).fields.field(1).name"));
+
+        config.addProperty("connection.user", "scott");
+        config.addProperty("connection.passwd", "tiger");
+        assertEquals("tiger", config.getProperty("connection.passwd"));
+
+        DefaultConfigurationKey key = createConfigurationKey();
+        key.append("tables").append("table").appendIndex(0);
+        key.appendAttribute("tableType");
+        config.addProperty(key.toString(), "system");
+        assertEquals("system", config.getProperty(key.toString()));
+    }
+
+    /**
+     * Creates a {@code DefaultConfigurationKey} object.
+     *
+     * @return the new key object
+     */
+    private static DefaultConfigurationKey createConfigurationKey()
+    {
+        return new DefaultConfigurationKey(DefaultExpressionEngine.INSTANCE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testAddPropertyInvalidKey()
+    {
+        config.addProperty(".", "InvalidKey");
+    }
+
+    @Test
+    public void testGetMaxIndex()
+    {
+        assertEquals(4, config.getMaxIndex("tables.table(0).fields.field"));
+        assertEquals(4, config.getMaxIndex("tables.table(1).fields.field"));
+        assertEquals(1, config.getMaxIndex("tables.table"));
+        assertEquals(1, config.getMaxIndex("tables.table.name"));
+        assertEquals(0, config.getMaxIndex("tables.table(0).name"));
+        assertEquals(0, config.getMaxIndex("tables.table(1).fields.field(1)"));
+        assertEquals(-1, config.getMaxIndex("tables.table(2).fields"));
+
+        int maxIdx = config.getMaxIndex("tables.table(0).fields.field.name");
+        for(int i = 0; i <= maxIdx; i++)
+        {
+            DefaultConfigurationKey key =
+                    new DefaultConfigurationKey(DefaultExpressionEngine.INSTANCE,
+                            "tables.table(0).fields");
+            key.append("field").appendIndex(i).append("name");
+            assertNotNull(config.getProperty(key.toString()));
+        }
+    }
+
+    @Test
+    public void testClone()
+    {
+        Configuration copy = (Configuration) config.clone();
+        assertTrue(copy instanceof BaseHierarchicalConfiguration);
+        checkContent(copy);
+    }
+
+    /**
+     * Tests whether registered event handlers are handled correctly when a
+     * configuration is cloned. They should not be registered at the clone.
+     */
+    @Test
+    public void testCloneWithEventListeners()
+    {
+        ConfigurationListener l = new ConfigurationListener()
+        {
+            public void configurationChanged(ConfigurationEvent event)
+            {
+                // just a dummy
+            }
+        };
+        config.addConfigurationListener(l);
+        BaseHierarchicalConfiguration copy = (BaseHierarchicalConfiguration) config
+                .clone();
+        assertFalse("Event listener registered at clone", copy
+                .getConfigurationListeners().contains(l));
+    }
+
+    /**
+     * Tests whether interpolation works as expected after cloning.
+     */
+    @Test
+    public void testCloneInterpolation()
+    {
+        final String keyAnswer = "answer";
+        final String keyValue = "value";
+        config.addProperty(keyAnswer, "The answer is ${" + keyValue + "}.");
+        config.addProperty(keyValue, 42);
+        BaseHierarchicalConfiguration clone =
+                (BaseHierarchicalConfiguration) config.clone();
+        clone.setProperty(keyValue, 43);
+        assertEquals("Wrong interpolation in original", "The answer is 42.",
+                config.getString(keyAnswer));
+        assertEquals("Wrong interpolation in clone", "The answer is 43.",
+                clone.getString(keyAnswer));
+    }
+
+    @Test @Ignore
+    public void testAddNodes()
+    {
+        //TODO rework
+//        Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+//        nodes.add(createFieldNode("birthDate"));
+//        nodes.add(createFieldNode("lastLogin"));
+//        nodes.add(createFieldNode("language"));
+//        config.addNodes("tables.table(0).fields", nodes);
+//        assertEquals(7, config.getMaxIndex("tables.table(0).fields.field"));
+//        assertEquals("birthDate", config.getString("tables.table(0).fields.field(5).name"));
+//        assertEquals("lastLogin", config.getString("tables.table(0).fields.field(6).name"));
+//        assertEquals("language", config.getString("tables.table(0).fields.field(7).name"));
+    }
+
+    /**
+     * Tests the addNodes() method when the provided key does not exist. In
+     * this case, a new node (or even a complete new branch) will be created.
+     */
+    @Test @Ignore
+    public void testAddNodesForNonExistingKey()
+    {
+//        Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+//        nodes.add(createNode("usr", "scott"));
+//        ConfigurationNode nd = createNode("pwd", "tiger");
+//        nd.setAttribute(true);
+//        nodes.add(nd);
+//        config.addNodes("database.connection.settings", nodes);
+//
+//        assertEquals("Usr node not found", "scott", config.getString("database.connection.settings.usr"));
+//        assertEquals("Pwd node not found", "tiger", config.getString("database.connection.settings[@pwd]"));
+    }
+
+    /**
+     * Tests the addNodes() method when the new nodes should be added to an
+     * attribute node. This is not allowed.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testAddNodesWithAttributeKey()
+    {
+        Collection<ImmutableNode> nodes = new ArrayList<ImmutableNode>();
+        nodes.add(NodeStructureHelper.createNode("testNode", "yes"));
+        config.addNodes("database.connection[@settings]", nodes);
+    }
+
+    /**
+     * Tests copying nodes from one configuration to another one.
+     */
+    @Test @Ignore
+    public void testAddNodesCopy()
+    {
+//        BaseHierarchicalConfiguration configDest = new BaseHierarchicalConfiguration();
+//        configDest.addProperty("test", "TEST");
+//        Collection<ConfigurationNode> nodes = config.getRootNode().getChildren();
+//        assertEquals("Wrong number of children", 1, nodes.size());
+//        configDest.addNodes("newNodes", nodes);
+//        for (int i = 0; i < tables.length; i++)
+//        {
+//            String keyTab = "newNodes.tables.table(" + i + ").";
+//            assertEquals("Table " + i + " not found", tables[i], configDest
+//                    .getString(keyTab + "name"));
+//            for (int j = 0; j < fields[i].length; j++)
+//            {
+//                assertEquals("Invalid field " + j + " in table " + i,
+//                        fields[i][j], configDest.getString(keyTab
+//                                + "fields.field(" + j + ").name"));
+//            }
+//        }
+    }
+
+    /**
+     * Tests adding an attribute node with the addNodes() method.
+     */
+    @Test @Ignore
+    public void testAddNodesAttributeNode()
+    {
+//        Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+//        ConfigurationNode nd = createNode("length", "10");
+//        nd.setAttribute(true);
+//        nodes.add(nd);
+//        config.addNodes("tables.table(0).fields.field(1)", nodes);
+//        assertEquals("Attribute was not added", "10", config
+//                .getString("tables.table(0).fields.field(1)[@length]"));
+    }
+
+    /**
+     * Tests setting a custom expression engine, which uses a slightly different
+     * syntax.
+     */
+    @Test
+    public void testSetExpressionEngine()
+    {
+        config.setExpressionEngine(null);
+        assertNotNull("Expression engine is null", config.getExpressionEngine());
+        assertSame("Default engine is not used",
+                DefaultExpressionEngine.INSTANCE, config.getExpressionEngine());
+
+        config.setExpressionEngine(createAlternativeExpressionEngine());
+        checkAlternativeSyntax();
+    }
+
+    /**
+     * Tests interpolation facilities.
+     */
+    @Test
+    public void testInterpolation()
+    {
+        config.addProperty("base.dir", "/home/foo");
+        config.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
+        config.addProperty("test.absolute.dir.dir2", "${base.dir}/path2");
+        config.addProperty("test.absolute.dir.dir3", "${base.dir}/path3");
+        //TODO check
+        Configuration sub = config.subset("test.absolute.dir");
+        for (int i = 1; i < 4; i++)
+        {
+            assertEquals("Wrong interpolation in parent", "/home/foo/path" + i,
+                    config.getString("test.absolute.dir.dir" + i));
+            assertEquals("Wrong interpolation in subnode",
+                    "/home/foo/path" + i, sub.getString("dir" + i));
+        }
+    }
+
+    /**
+     * Basic interpolation tests.
+     */
+    @Test
+    public void testInterpolationBasic()
+    {
+        InterpolationTestHelper.testInterpolation(config);
+    }
+
+    /**
+     * Tests multiple levels of interpolation.
+     */
+    @Test
+    public void testInterpolationMultipleLevels()
+    {
+        InterpolationTestHelper.testMultipleInterpolation(config);
+    }
+
+    /**
+     * Tests an invalid interpolation that causes an endless loop.
+     */
+    @Test
+    public void testInterpolationLoop()
+    {
+        InterpolationTestHelper.testInterpolationLoop(config);
+    }
+
+    /**
+     * Tests interpolation with a subset.
+     */
+    @Test
+    public void testInterpolationSubset()
+    {
+        InterpolationTestHelper.testInterpolationSubset(config);
+    }
+
+    /**
+     * Tests whether interpolation with a subset configuration works over
+     * multiple layers.
+     */
+    @Test
+    public void testInterpolationSubsetMultipleLayers()
+    {
+        config.clear();
+        config.addProperty("var", "value");
+        config.addProperty("prop2.prop[@attr]", "${var}");
+        Configuration sub1 = config.subset("prop2");
+        Configuration sub2 = sub1.subset("prop");
+        assertEquals("Wrong value", "value", sub2.getString("[@attr]"));
+    }
+
+    /**
+     * Tests interpolation of a variable, which cannot be resolved.
+     */
+    @Test
+    public void testInterpolationUnknownProperty()
+    {
+        InterpolationTestHelper.testInterpolationUnknownProperty(config);
+    }
+
+    /**
+     * Tests interpolation with system properties.
+     */
+    @Test
+    public void testInterpolationSysProperties()
+    {
+        InterpolationTestHelper.testInterpolationSystemProperties(config);
+    }
+
+    /**
+     * Tests interpolation with constant values.
+     */
+    @Test
+    public void testInterpolationConstants()
+    {
+        InterpolationTestHelper.testInterpolationConstants(config);
+    }
+
+    /**
+     * Tests escaping variables.
+     */
+    @Test
+    public void testInterpolationEscaped()
+    {
+        InterpolationTestHelper.testInterpolationEscaped(config);
+    }
+
+    /**
+     * Tests manipulating the interpolator.
+     */
+    @Test
+    public void testInterpolator()
+    {
+        InterpolationTestHelper.testGetInterpolator(config);
+    }
+
+    /**
+     * Tests obtaining a configuration with all variables substituted.
+     */
+    @Test
+    public void testInterpolatedConfiguration()
+    {
+        config.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
+        AbstractHierarchicalConfiguration<?> c = (AbstractHierarchicalConfiguration<?>) InterpolationTestHelper
+                .testInterpolatedConfiguration(config);
+
+        // tests whether the hierarchical structure has been maintained
+        checkGetProperty(c);
+    }
+
+    /**
+     * Tests the copy constructor when a null reference is passed.
+     */
+    @Test
+    public void testInitCopyNull()
+    {
+        BaseHierarchicalConfiguration copy =
+                new BaseHierarchicalConfiguration(
+                        (HierarchicalConfiguration) null);
+        assertTrue("Configuration not empty", copy.isEmpty());
+    }
+
+    /**
+     * Tests whether keys that contains brackets can be used.
+     */
+    @Test
+    public void testGetPropertyKeyWithBrackets()
+    {
+        final String key = "test.directory.platform(x86)";
+        config.addProperty(key, "C:\\Temp");
+        assertEquals("Wrong property value", "C:\\Temp", config.getString(key));
+    }
+
+    /**
+     * Helper method for testing the getKeys(String) method.
+     *
+     * @param prefix the key to pass into getKeys()
+     * @param expected the expected result
+     */
+    private void checkKeys(String prefix, String[] expected)
+    {
+        Set<String> values = new HashSet<String>();
+        for (String anExpected : expected) {
+            values.add((anExpected.startsWith(prefix)) ? anExpected : prefix + "." + anExpected);
+        }
+
+        Iterator<String> itKeys = config.getKeys(prefix);
+        while(itKeys.hasNext())
+        {
+            String key = itKeys.next();
+            if(!values.contains(key))
+            {
+                fail("Found unexpected key: " + key);
+            }
+            else
+            {
+                values.remove(key);
+            }
+        }
+
+        assertTrue("Remaining keys " + values, values.isEmpty());
+    }
+
+    /**
+     * Helper method for checking keys using an alternative syntax.
+     */
+    private void checkAlternativeSyntax()
+    {
+        assertNull(config.getProperty("tables/table/resultset"));
+        assertNull(config.getProperty("tables/table/fields/field"));
+
+        Object prop = config.getProperty("tables/table[0]/fields/field/name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(5, ((Collection<?>) prop).size());
+
+        prop = config.getProperty("tables/table/fields/field/name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(10, ((Collection<?>) prop).size());
+
+        prop = config.getProperty("tables/table/fields/field[3]/name");
+        assertNotNull(prop);
+        assertTrue(prop instanceof Collection);
+        assertEquals(2, ((Collection<?>) prop).size());
+
+        prop = config.getProperty("tables/table[1]/fields/field[2]/name");
+        assertNotNull(prop);
+        assertEquals("creationDate", prop.toString());
+
+        Set<String> keys = new HashSet<String>();
+        CollectionUtils.addAll(keys, config.getKeys());
+        assertEquals("Wrong number of defined keys", 2, keys.size());
+        assertTrue("Key not found", keys.contains("tables/table/name"));
+        assertTrue("Key not found", keys
+                .contains("tables/table/fields/field/name"));
+    }
+
+    /**
+     * Checks the content of the passed in configuration object. Used by some
+     * tests that copy a configuration.
+     *
+     * @param c the configuration to check
+     */
+    private static void checkContent(Configuration c)
+    {
+        for (int i = 0; i < NodeStructureHelper.tablesLength(); i++)
+        {
+            assertEquals(NodeStructureHelper.table(i),
+                    c.getString("tables.table(" + i + ").name"));
+            for (int j = 0; j < NodeStructureHelper.fieldsLength(i); j++)
+            {
+                assertEquals(
+                        NodeStructureHelper.field(i, j),
+                        c.getString("tables.table(" + i + ").fields.field(" + j
+                                + ").name"));
+            }
+        }
+    }
+
+    private ExpressionEngine createAlternativeExpressionEngine()
+    {
+        return new DefaultExpressionEngine(
+                new DefaultExpressionEngineSymbols.Builder(
+                        DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS)
+                        .setPropertyDelimiter("/").setIndexStart("[")
+                        .setIndexEnd("]").create());
+    }
+
+    /**
+     * A concrete test implementation of {@code AbstractHierarchicalConfiguration}.
+     */
+    private static class AbstractHierarchicalConfigurationTestImpl extends AbstractHierarchicalConfiguration<ImmutableNode> {
+        public AbstractHierarchicalConfigurationTestImpl(InMemoryNodeModel model) {
+            super(model);
+        }
+
+        @Override
+        protected NodeModel<ImmutableNode> cloneNodeModel() {
+            return null;
+        }
+    }
+}