/*
 * 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.builder.combined;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
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.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
import org.apache.commons.configuration2.CombinedConfiguration;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.ConfigurationAssert;
import org.apache.commons.configuration2.ConfigurationDecoder;
import org.apache.commons.configuration2.DynamicCombinedConfiguration;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.XMLConfiguration;
import org.apache.commons.configuration2.XMLPropertiesConfiguration;
import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
import org.apache.commons.configuration2.builder.BuilderEventListenerImpl;
import org.apache.commons.configuration2.builder.ConfigurationBuilder;
import org.apache.commons.configuration2.builder.ConfigurationBuilderEvent;
import org.apache.commons.configuration2.builder.CopyObjectDefaultHandler;
import org.apache.commons.configuration2.builder.FileBasedBuilderParametersImpl;
import org.apache.commons.configuration2.builder.FileBasedBuilderProperties;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.PropertiesBuilderParametersImpl;
import org.apache.commons.configuration2.builder.ReloadingFileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.XMLBuilderParametersImpl;
import org.apache.commons.configuration2.builder.XMLBuilderProperties;
import org.apache.commons.configuration2.builder.fluent.FileBasedBuilderParameters;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
import org.apache.commons.configuration2.event.ConfigurationEvent;
import org.apache.commons.configuration2.event.Event;
import org.apache.commons.configuration2.event.EventListener;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
import org.apache.commons.configuration2.interpol.Lookup;
import org.apache.commons.configuration2.io.DefaultFileSystem;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.configuration2.io.FileLocatorUtils;
import org.apache.commons.configuration2.io.FileSystem;
import org.apache.commons.configuration2.reloading.ReloadingController;
import org.apache.commons.configuration2.reloading.ReloadingControllerSupport;
import org.apache.commons.configuration2.resolver.CatalogResolver;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Test class for {@code CombinedConfigurationBuilder}.
 *
 * @version $Id: TestCombinedConfigurationBuilder.java 1638384 2014-11-11 20:48:16Z oheger $
 */
public class TestCombinedConfigurationBuilder
{
    /** Test configuration definition file. */
    private static final File TEST_FILE = ConfigurationAssert
            .getTestFile("testDigesterConfiguration.xml");

    /** Test file name for a sub configuration. */
    private static final String TEST_SUB_XML = "test.xml";

    /** Constant for a named builder. */
    private static final String BUILDER_NAME = "subBuilderName";

    /**
     * The name of the system property for selecting a file managed by a
     * MultiFileConfigurationBuilder.
     */
    private static final String MULTI_FILE_PROPERTY = "Id";

    /** A helper object for creating builder parameters. */
    protected Parameters parameters;

    /** Stores the object to be tested. */
    protected CombinedConfigurationBuilder builder;

    @Before
    public void setUp() throws Exception
    {
        System.setProperty("java.naming.factory.initial",
                "org.apache.commons.configuration2.MockInitialContextFactory");
        System.setProperty("test_file_xml", TEST_SUB_XML);
        System.setProperty("test_file_combine", "testcombine1.xml");
        parameters = new Parameters();
        builder = new CombinedConfigurationBuilder();
    }

    @After
    public void tearDown() throws Exception
    {
        System.getProperties().remove(MULTI_FILE_PROPERTY);
    }

    /**
     * Creates a configuration builder for the definition configuration which
     * always returns the passed in definition configuration.
     *
     * @param defConfig the definition configuration
     * @return the definition builder
     */
    protected static BasicConfigurationBuilder<? extends BaseHierarchicalConfiguration> createDefinitionBuilder(
            BaseHierarchicalConfiguration defConfig)
    {
        return new ConstantConfigurationBuilder(defConfig);
    }

    /**
     * Convenience method for creating a definition configuration. This method
     * creates a configuration containing a tag referring to a configuration
     * source. The tag has attributes defined by the given map.
     *
     * @param tag the name of the tag to create
     * @param attrs the attributes of this tag
     * @return the definition configuration
     */
    protected static BaseHierarchicalConfiguration createDefinitionConfig(String tag,
            Map<String, Object> attrs)
    {
        BaseHierarchicalConfiguration defConfig =
                new BaseHierarchicalConfiguration();
        String prefix = "override." + tag;
        for (Map.Entry<String, Object> e : attrs.entrySet())
        {
            defConfig.addProperty(prefix + "[@" + e.getKey() + "]",
                    e.getValue());
        }
        return defConfig;
    }

    /**
     * Creates an object with parameters for defining the file to be loaded.
     *
     * @return the parameters object
     */
    protected FileBasedBuilderParameters createParameters()
    {
        return parameters.fileBased();
    }

    /**
     * Tries to build a configuration if no definition builder is provided.
     */
    @Test(expected = ConfigurationException.class)
    public void testNoDefinitionBuilder() throws ConfigurationException
    {
        builder.getConfiguration();
    }

    /**
     * Tests if the configuration was correctly created by the builder.
     *
     * @return the combined configuration obtained from the builder
     */
    private CombinedConfiguration checkConfiguration()
            throws ConfigurationException
    {
        CombinedConfiguration compositeConfiguration =
                builder.getConfiguration();

        assertEquals("Number of configurations", 3,
                compositeConfiguration.getNumberOfConfigurations());
        assertEquals(PropertiesConfiguration.class, compositeConfiguration
                .getConfiguration(0).getClass());
        assertEquals(XMLPropertiesConfiguration.class, compositeConfiguration
                .getConfiguration(1).getClass());
        assertEquals(XMLConfiguration.class, compositeConfiguration
                .getConfiguration(2).getClass());

        // check the first configuration
        PropertiesConfiguration pc =
                (PropertiesConfiguration) compositeConfiguration
                        .getConfiguration(0);
        assertNotNull("No properties configuration", pc);

        // check some properties
        checkProperties(compositeConfiguration);
        return compositeConfiguration;
    }

    /**
     * Checks if the passed in configuration contains the expected properties.
     *
     * @param compositeConfiguration the configuration to check
     */
    private void checkProperties(Configuration compositeConfiguration)
    {
        assertTrue("Make sure we have loaded our key",
                compositeConfiguration.getBoolean("test.boolean"));
        assertEquals("I'm complex!",
                compositeConfiguration
                        .getProperty("element2.subelement.subsubelement"));
        assertEquals("property in the XMLPropertiesConfiguration", "value1",
                compositeConfiguration.getProperty("key1"));
    }

    /**
     * Tests that the return value of configure() is overloaded.
     */
    @Test
    public void testConfigureResult()
    {
        CombinedConfigurationBuilder configuredBuilder =
                builder.configure(createParameters().setFile(TEST_FILE));
        assertSame("Wrong instance returned", builder, configuredBuilder);
    }

    /**
     * Tests loading a simple configuration definition file.
     */
    @Test
    public void testLoadConfiguration() throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        checkConfiguration();
    }

    /**
     * Tests loading a configuration definition file with an additional section.
     */
    @Test
    public void testLoadAdditional() throws ConfigurationException
    {
        File additonalFile =
                ConfigurationAssert
                        .getTestFile("testDigesterConfiguration2.xml");
        builder.configure(createParameters()
                .setFile(additonalFile));
        CombinedConfiguration compositeConfiguration =
                builder.getConfiguration();
        assertEquals("Verify how many configs", 2,
                compositeConfiguration.getNumberOfConfigurations());

        // Test if union was constructed correctly
        Object prop = compositeConfiguration.getProperty("tables.table.name");
        assertTrue(prop instanceof Collection);
        assertEquals(3, ((Collection<?>) prop).size());
        assertEquals("users",
                compositeConfiguration.getProperty("tables.table(0).name"));
        assertEquals("documents",
                compositeConfiguration.getProperty("tables.table(1).name"));
        assertEquals("tasks",
                compositeConfiguration.getProperty("tables.table(2).name"));

        prop =
                compositeConfiguration
                        .getProperty("tables.table.fields.field.name");
        assertTrue(prop instanceof Collection);
        assertEquals(17, ((Collection<?>) prop).size());

        assertEquals("smtp.mydomain.org",
                compositeConfiguration.getString("mail.host.smtp"));
        assertEquals("pop3.mydomain.org",
                compositeConfiguration.getString("mail.host.pop"));

        // This was overriden
        assertEquals("masterOfPost",
                compositeConfiguration.getString("mail.account.user"));
        assertEquals("topsecret",
                compositeConfiguration.getString("mail.account.psswd"));

        // This was overridden, too, but not in additional section
        assertEquals("enhanced factory",
                compositeConfiguration.getString("test.configuration"));
    }

    /**
     * Tests loading a definition file that contains optional configurations.
     */
    @Test
    public void testLoadOptional() throws Exception
    {
        File optionalFile =
                ConfigurationAssert
                        .getTestFile("testDigesterOptionalConfiguration.xml");
        builder.configure(createParameters()
                .setFile(optionalFile));
        Configuration config = builder.getConfiguration();
        assertTrue(config.getBoolean("test.boolean"));
        assertEquals("value", config.getProperty("element"));
    }

    /**
     * Tests loading a definition file with optional and non optional
     * configuration sources. One non optional does not exist, so this should
     * cause an exception.
     */
    @Test(expected = ConfigurationException.class)
    public void testLoadOptionalWithException() throws ConfigurationException
    {
        File optionalExFile =
                ConfigurationAssert
                        .getTestFile("testDigesterOptionalConfigurationEx.xml");
        builder.configure(createParameters()
                .setFile(optionalExFile));
        builder.getConfiguration();
    }

    /**
     * Tests whether the force-create attribute is taken into account.
     */
    @Test
    public void testLoadOptionalForceCreate() throws ConfigurationException
    {
        String name = "optionalConfig";
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("fileName", "nonExisting.xml");
        attrs.put("config-name", name);
        attrs.put("config-optional", Boolean.TRUE);
        attrs.put("config-forceCreate", Boolean.TRUE);
        BaseHierarchicalConfiguration defConfig =
                createDefinitionConfig("xml", attrs);
        BasicConfigurationBuilder<? extends BaseHierarchicalConfiguration> defBuilder =
                createDefinitionBuilder(defConfig);
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilder(defBuilder));
        CombinedConfiguration cc = builder.getConfiguration();
        assertEquals("Wrong number of configurations", 1,
                cc.getNumberOfConfigurations());
        assertTrue("Wrong configuration type",
                cc.getConfiguration(name) instanceof XMLConfiguration);
    }

    /**
     * Tests the behavior of builderNames() before the result configuration has
     * been created.
     */
    @Test
    public void testBuilderNamesBeforeConfigurationAccess()
    {
        assertTrue("Got builders (1)", builder.builderNames().isEmpty());
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        assertTrue("Got builders (2)", builder.builderNames().isEmpty());
    }

    /**
     * Tests whether the names of sub builders can be queried.
     */
    @Test
    public void testBuilderNames() throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        builder.getConfiguration();
        Set<String> names = builder.builderNames();
        List<String> expected = Arrays.asList("props", "xml");
        assertEquals("Wrong number of named builders", expected.size(),
                names.size());
        assertTrue("Wrong builder names: " + names, names.containsAll(expected));
    }

    /**
     * Tests that the collection with builder names cannot be manipulated.
     */
    @Test(expected = UnsupportedOperationException.class)
    public void testBuilderNamesManipulate() throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        builder.getConfiguration();
        Set<String> names = builder.builderNames();
        names.add(BUILDER_NAME);
    }

    /**
     * Tests whether named builders can be accessed.
     */
    @Test
    public void testGetNamedBuilder() throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        builder.getConfiguration();
        ConfigurationBuilder<? extends Configuration> propBuilder =
                builder.getNamedBuilder("props");
        assertTrue("Wrong builder class",
                propBuilder instanceof FileBasedConfigurationBuilder);
        assertTrue(
                "Wrong sub configuration",
                propBuilder.getConfiguration() instanceof PropertiesConfiguration);
    }

    /**
     * Tries to query a non-existing builder by name.
     */
    @Test(expected = ConfigurationException.class)
    public void testGetNamedBuilderUnknown() throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        builder.getConfiguration();
        builder.getNamedBuilder("nonExistingBuilder");
    }

    /**
     * Tries to query a named builder before the result configuration has been
     * created.
     */
    @Test(expected = ConfigurationException.class)
    public void testGetNamedBuilderBeforeConfigurationAccess()
            throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        builder.getNamedBuilder("nonExistingBuilder");
    }

    /**
     * Prepares a test with a combined configuration that uses a single sub
     * builder. This method adds some default attributes to the given map,
     * creates the corresponding definition builder and configures the combined
     * builder.
     *
     * @param attrs the map with attributes
     * @return the definition builder
     */
    private BasicConfigurationBuilder<? extends HierarchicalConfiguration<ImmutableNode>> prepareSubBuilderTest(
            Map<String, Object> attrs)
    {
        attrs.put("fileName", TEST_SUB_XML);
        attrs.put("config-name", BUILDER_NAME);
        BaseHierarchicalConfiguration defConfig =
                createDefinitionConfig("xml", attrs);
        BasicConfigurationBuilder<? extends HierarchicalConfiguration<ImmutableNode>> defBuilder =
                createDefinitionBuilder(defConfig);
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilder(defBuilder));
        return defBuilder;
    }

    /**
     * Tests a reset of the builder. The configuration instance should be
     * created anew.
     */
    @Test
    public void testResetBuilder() throws ConfigurationException
    {
        Map<String, Object> attrs = new HashMap<String, Object>();
        BasicConfigurationBuilder<? extends HierarchicalConfiguration<ImmutableNode>> defBuilder =
                prepareSubBuilderTest(attrs);
        CombinedConfiguration cc = builder.getConfiguration();
        ConfigurationBuilder<? extends Configuration> subBuilder =
                builder.getNamedBuilder(BUILDER_NAME);
        defBuilder.reset();
        CombinedConfiguration cc2 = builder.getConfiguration();
        assertNotSame("No new configuration instance", cc, cc2);
        ConfigurationBuilder<? extends Configuration> subBuilder2 =
                builder.getNamedBuilder(BUILDER_NAME);
        assertNotSame("No new sub builder instance", subBuilder, subBuilder2);
    }

    /**
     * Tests whether a reloading sub builder can be created.
     */
    @Test
    public void testReloadingBuilder() throws ConfigurationException
    {
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("config-reload", Boolean.TRUE);
        prepareSubBuilderTest(attrs);
        builder.getConfiguration();
        assertTrue(
                "Not a reloading builder",
                builder.getNamedBuilder(BUILDER_NAME) instanceof ReloadingFileBasedConfigurationBuilder);
    }

    /**
     * Tests whether a reset of one of the sub builders causes the combined
     * configuration to be re-created.
     */
    @Test
    public void testReactOnSubBuilderChange() throws ConfigurationException
    {
        Map<String, Object> attrs = new HashMap<String, Object>();
        prepareSubBuilderTest(attrs);
        CombinedConfiguration cc = builder.getConfiguration();
        BasicConfigurationBuilder<?> subBuilder =
                (BasicConfigurationBuilder<?>) builder
                        .getNamedBuilder(BUILDER_NAME);
        subBuilder.reset();
        assertNotSame("Configuration not newly created", cc,
                builder.getConfiguration());
    }

    /**
     * Tests that change listeners registered at sub builders are removed on a
     * reset.
     */
    @Test
    public void testRemoveSubBuilderListener() throws ConfigurationException
    {
        Map<String, Object> attrs = new HashMap<String, Object>();
        prepareSubBuilderTest(attrs);
        builder.getConfiguration();
        BasicConfigurationBuilder<?> subBuilder =
                (BasicConfigurationBuilder<?>) builder
                        .getNamedBuilder(BUILDER_NAME);
        builder.reset();
        prepareSubBuilderTest(attrs);
        CombinedConfiguration cc = builder.getConfiguration();
        BasicConfigurationBuilder<?> subBuilder2 =
                (BasicConfigurationBuilder<?>) builder
                        .getNamedBuilder(BUILDER_NAME);
        assertNotSame("Got the same sub builder", subBuilder, subBuilder2);
        subBuilder.reset();
        assertSame("Configuration was reset", cc, builder.getConfiguration());
    }

    /**
     * Helper method for testing the attributes of a combined configuration
     * created by the builder.
     *
     * @param cc the configuration to be checked
     */
    private static void checkCombinedConfigAttrs(CombinedConfiguration cc)
    {
        ListDelimiterHandler handler = cc.getListDelimiterHandler();
        assertTrue("Wrong delimiter handler: " + handler,
                handler instanceof DefaultListDelimiterHandler);
        assertEquals("Wrong list delimiter character", ',',
                ((DefaultListDelimiterHandler) handler).getDelimiter());
    }

    /**
     * Tests whether attributes are correctly set on the combined configurations
     * for the override and additional sections.
     */
    @Test
    public void testCombinedConfigurationAttributes()
            throws ConfigurationException
    {
        File initFile =
                ConfigurationAssert
                        .getTestFile("testCCResultInitialization.xml");
        builder.configure(createParameters()
                .setFile(initFile));
        CombinedConfiguration cc = builder.getConfiguration();
        checkCombinedConfigAttrs(cc);
        CombinedConfiguration cc2 =
                (CombinedConfiguration) cc
                        .getConfiguration(CombinedConfigurationBuilder.ADDITIONAL_NAME);
        checkCombinedConfigAttrs(cc2);
    }

    /**
     * Tests the structure of the returned combined configuration if there is no
     * additional section.
     */
    @Test
    public void testCombinedConfigurationNoAdditional()
            throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        CombinedConfiguration cc = builder.getConfiguration();
        assertNull(
                "Additional configuration was found",
                cc.getConfiguration(CombinedConfigurationBuilder.ADDITIONAL_NAME));
    }

    /**
     * Tests whether the list node definition was correctly processed.
     */
    @Test
    public void testCombinedConfigurationListNodes()
            throws ConfigurationException
    {
        File initFile =
                ConfigurationAssert
                        .getTestFile("testCCResultInitialization.xml");
        builder.configure(createParameters()
                .setFile(initFile));
        CombinedConfiguration cc = builder.getConfiguration();
        Set<String> listNodes = cc.getNodeCombiner().getListNodes();
        assertEquals("Wrong number of list nodes", 2, listNodes.size());
        assertTrue("table node not a list node", listNodes.contains("table"));
        assertTrue("list node not a list node", listNodes.contains("list"));

        CombinedConfiguration cca =
                (CombinedConfiguration) cc
                        .getConfiguration(CombinedConfigurationBuilder.ADDITIONAL_NAME);
        listNodes = cca.getNodeCombiner().getListNodes();
        assertTrue("Found list nodes for additional combiner",
                listNodes.isEmpty());
    }

    /**
     * Tests whether a custom provider can be registered.
     */
    @Test
    public void testCustomBuilderProvider() throws ConfigurationException
    {
        String tagName = "myTestTag";
        final BaseHierarchicalConfiguration dataConf =
                new BaseHierarchicalConfiguration();
        dataConf.addProperty(tagName, Boolean.TRUE);
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("config-name", BUILDER_NAME);
        attrs.put("config-at", "tests");
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilder(
                        createDefinitionBuilder(createDefinitionConfig(tagName,
                                attrs))).registerProvider(tagName,
                        new ConfigurationBuilderProvider()
                        {
                            @Override
                            public ConfigurationBuilder<? extends Configuration> getConfigurationBuilder(
                                    ConfigurationDeclaration decl)
                                    throws ConfigurationException
                            {
                                return new ConstantConfigurationBuilder(
                                        dataConf);
                            }
                        }));
        CombinedConfiguration cc = builder.getConfiguration();
        assertEquals("Configuration not added", dataConf,
                cc.getConfiguration(BUILDER_NAME));
        assertEquals("Property not set", Boolean.TRUE,
                cc.getProperty("tests." + tagName));
    }

    /**
     * Tests whether a custom provider can be defined in the definition file.
     */
    @Test
    public void testProviderInDefinitionConfig() throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(ConfigurationAssert
                        .getTestFile("testCCCustomProvider.xml")));
        CombinedConfiguration cc = builder.getConfiguration();
        assertTrue("Property not found", cc.getBoolean("testKey"));
    }

    /**
     * Tests whether a file with system properties can be specified in the
     * configuration definition file and that system properties can be added to
     * the resulting configuration.
     */
    @Test
    public void testSystemProperties() throws ConfigurationException
    {
        File systemFile =
                ConfigurationAssert.getTestFile("testCCSystemProperties.xml");
        builder.configure(createParameters()
                .setFile(systemFile));
        CombinedConfiguration cc = builder.getConfiguration();
        assertTrue("System property not found", cc.containsKey("user.name"));
        assertEquals("Properties not added", "value1",
                System.getProperty("key1"));
    }

    /**
     * Tests whether environment properties can be added as a configuration
     * source.
     */
    @Test
    public void testEnvironmentProperties() throws ConfigurationException
    {
        File envFile =
                ConfigurationAssert.getTestFile("testCCEnvProperties.xml");
        builder.configure(createParameters().setFile(envFile));
        CombinedConfiguration cc = builder.getConfiguration();
        assertFalse("Configuration is empty", cc.isEmpty());
        for (Map.Entry<String, String> e : System.getenv().entrySet())
        {
            assertEquals("Wrong value for property: " + e.getKey(),
                    e.getValue(), cc.getString(e.getKey()));
        }
    }

    /**
     * Tests whether a JNDI configuration can be integrated into the combined
     * configuration.
     */
    @Test
    public void testJndiConfiguration() throws ConfigurationException
    {
        File multiFile =
                ConfigurationAssert
                        .getTestFile("testDigesterConfiguration3.xml");
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilderParameters(createParameters()
                        .setFile(multiFile)));
        CombinedConfiguration cc = builder.getConfiguration();
        assertTrue("JNDI property not found", cc.getBoolean("test.onlyinjndi"));
    }

    /**
     * Tests whether an INI configuration source can be added to the combined
     * configuration.
     */
    @Test
    public void testINIConfiguration() throws ConfigurationException
    {
        File multiFile =
                ConfigurationAssert
                        .getTestFile("testDigesterConfiguration3.xml");
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilderParameters(createParameters()
                        .setFile(multiFile)));
        CombinedConfiguration cc = builder.getConfiguration();
        assertEquals("Property from ini file not found", "yes",
                cc.getString("testini.loaded"));
    }

    /**
     * Tests whether an entity resolver can be defined in the definition file.
     */
    @Test
    public void testCustomEntityResolver() throws ConfigurationException
    {
        File resolverFile =
                ConfigurationAssert.getTestFile("testCCEntityResolver.xml");
        builder.configure(createParameters()
                .setFile(resolverFile));
        CombinedConfiguration cc = builder.getConfiguration();
        XMLConfiguration xmlConf =
                (XMLConfiguration) cc.getConfiguration("xml");
        EntityResolverWithPropertiesTestImpl resolver =
                (EntityResolverWithPropertiesTestImpl) xmlConf
                        .getEntityResolver();
        assertFalse("No lookups", resolver.getInterpolator().getLookups()
                .isEmpty());
    }

    /**
     * Tests whether the entity resolver is initialized with other XML-related
     * properties.
     */
    @Test
    public void testConfigureEntityResolverWithProperties()
            throws ConfigurationException
    {
        HierarchicalConfiguration<ImmutableNode> config = new BaseHierarchicalConfiguration();
        config.addProperty("header.entity-resolver[@config-class]",
                EntityResolverWithPropertiesTestImpl.class.getName());
        XMLBuilderParametersImpl xmlParams = new XMLBuilderParametersImpl();
        FileSystem fs = EasyMock.createMock(FileSystem.class);
        String baseDir = ConfigurationAssert.OUT_DIR_NAME;
        xmlParams.setBasePath(baseDir);
        xmlParams.setFileSystem(fs);
        builder.configureEntityResolver(config, xmlParams);
        EntityResolverWithPropertiesTestImpl resolver =
                (EntityResolverWithPropertiesTestImpl) xmlParams
                        .getEntityResolver();
        assertSame("File system not set", fs, resolver.getFileSystem());
        assertSame("Base directory not set", baseDir, resolver.getBaseDir());
    }

    /**
     * Helper method for testing whether the file system can be customized in
     * the configuration definition file.
     *
     * @param fsFile the file to be processed
     * @throws ConfigurationException if an error occurs
     */
    private void checkFileSystem(File fsFile) throws ConfigurationException
    {
        builder.configure(createParameters().setFile(fsFile));
        builder.getConfiguration();
        @SuppressWarnings("unchecked") // this is the minimum bound for type arguments
        FileBasedConfigurationBuilder<? extends Configuration> xmlBuilder =
                (FileBasedConfigurationBuilder<? extends Configuration>) builder
                        .getNamedBuilder("xml");
        assertTrue("Wrong file system: "
                + xmlBuilder.getFileHandler().getFileSystem(), xmlBuilder
                .getFileHandler().getFileSystem() instanceof FileSystemTestImpl);
    }

    /**
     * Tests whether a default file system can be configured in the definition
     * file.
     */
    @Test
    public void testCustomFileSystem() throws ConfigurationException
    {
        checkFileSystem(ConfigurationAssert.getTestFile("testCCFileSystem.xml"));
    }

    /**
     * Tests whether a specific file system can be applied to a sub
     * configuration.
     */
    @Test
    public void testCustomFileSystemForSubConfig()
            throws ConfigurationException
    {
        checkFileSystem(ConfigurationAssert
                .getTestFile("testCCFileSystemSubConfig.xml"));
    }

    /**
     * Tests whether a default base path for all file-based child configurations
     * can be set in the builder parameters.
     */
    @Test
    public void testDefaultBasePathInParameters() throws ConfigurationException
    {
        File testFile =
                ConfigurationAssert.getTestFile("testCCSystemProperties.xml");
        String basePath = ConfigurationAssert.OUT_DIR.getAbsolutePath();
        builder.configure(new CombinedBuilderParametersImpl().setBasePath(
                basePath).setDefinitionBuilderParameters(
                createParameters().setFile(testFile)));
        builder.getConfiguration();
        XMLBuilderParametersImpl xmlParams = new XMLBuilderParametersImpl();
        builder.initChildBuilderParameters(xmlParams);
        assertEquals("Base path not set", basePath, xmlParams.getFileHandler()
                .getBasePath());
    }

    /**
     * Tests whether the default base path for file-based configurations is
     * derived from the configuration definition builder.
     */
    @Test
    public void testDefaultBasePathFromDefinitionBuilder()
            throws ConfigurationException, IOException
    {
        String testFile = "testCCSystemProperties.xml";
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilderParameters(createParameters()
                        .setBasePath(
                                ConfigurationAssert.TEST_DIR.getAbsolutePath())
                        .setFileName(testFile)));
        builder.getConfiguration();
        XMLBuilderParametersImpl xmlParams = new XMLBuilderParametersImpl();
        builder.initChildBuilderParameters(xmlParams);
        File basePathFile =
                FileLocatorUtils.fileFromURL(new URL(xmlParams
                        .getFileHandler().getBasePath()));
        assertEquals("Wrong base path",
                ConfigurationAssert.getTestFile(testFile).getAbsoluteFile(),
                basePathFile);
    }

    /**
     * Tests if the base path is correctly evaluated.
     */
    @Test
    public void testBasePathForChildConfigurations()
            throws ConfigurationException
    {
        BaseHierarchicalConfiguration defConfig =
                new BaseHierarchicalConfiguration();
        defConfig.addProperty("properties[@fileName]", "test.properties");
        File deepDir = new File(ConfigurationAssert.TEST_DIR, "config/deep");
        builder.configure(new CombinedBuilderParametersImpl().setBasePath(
                deepDir.getAbsolutePath()).setDefinitionBuilder(
                new ConstantConfigurationBuilder(defConfig)));
        CombinedConfiguration config = builder.getConfiguration();
        assertEquals("Wrong property value", "somevalue",
                config.getString("somekey"));
    }

    /**
     * Tests whether the resulting combined configuration can be customized.
     */
    @Test
    public void testCustomResultConfiguration() throws ConfigurationException
    {
        File testFile =
                ConfigurationAssert.getTestFile("testCCResultClass.xml");
        ListDelimiterHandler listHandler = new DefaultListDelimiterHandler('.');
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilderParameters(
                        new XMLBuilderParametersImpl().setFile(testFile))
                .setListDelimiterHandler(listHandler)
                .setThrowExceptionOnMissing(false));
        CombinedConfiguration cc = builder.getConfiguration();
        assertTrue("Wrong configuration class: " + cc.getClass(),
                cc instanceof CombinedConfigurationTestImpl);
        assertTrue("Wrong exception flag", cc.isThrowExceptionOnMissing());
        assertEquals("Wrong list delimiter handler", listHandler,
                cc.getListDelimiterHandler());
    }

    /**
     * Tests whether a configuration builder can itself be declared in a
     * configuration definition file.
     */
    @Test
    public void testConfigurationBuilderProvider()
            throws ConfigurationException
    {
        BaseHierarchicalConfiguration defConfig =
                new BaseHierarchicalConfiguration();
        defConfig.addProperty("override.configuration[@fileName]",
                TEST_FILE.getAbsolutePath());
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilder(new ConstantConfigurationBuilder(
                        defConfig)));
        CombinedConfiguration cc = builder.getConfiguration();
        assertEquals("Wrong number of configurations", 1,
                cc.getNumberOfConfigurations());
        checkProperties(cc);
    }

    /**
     * Tests whether basic properties defined for the combined configuration are
     * inherited by a child combined configuration builder.
     */
    @Test
    public void testConfigurationBuilderProviderInheritBasicProperties()
            throws ConfigurationException
    {
        File testFile =
                ConfigurationAssert
                        .getTestFile("testCCCombinedChildBuilder.xml");
        ListDelimiterHandler listHandler = new DefaultListDelimiterHandler('*');
        ConfigurationDecoder decoder = EasyMock.createMock(ConfigurationDecoder.class);
        builder.configure(new CombinedBuilderParametersImpl()
                .setDefinitionBuilderParameters(
                        new XMLBuilderParametersImpl().setFile(testFile))
                .setListDelimiterHandler(listHandler)
                .setConfigurationDecoder(decoder));
        CombinedConfiguration cc = builder.getConfiguration();
        CombinedConfiguration cc2 =
                (CombinedConfiguration) cc.getConfiguration("subcc");
        assertFalse("Wrong exception flag", cc2.isThrowExceptionOnMissing());
        assertEquals("Wrong list delimiter handler", listHandler,
                cc2.getListDelimiterHandler());
        assertEquals("Wrong decoder", decoder, cc2.getConfigurationDecoder());
    }

    /**
     * Tests whether a child configuration builder inherits the event listeners
     * from its parent.
     */
    @Test
    public void testConfigurationBuilderProviderInheritEventListeners()
            throws ConfigurationException
    {
        @SuppressWarnings("unchecked")
        EventListener<Event> l1 = EasyMock.createNiceMock(EventListener.class);
        @SuppressWarnings("unchecked")
        EventListener<ConfigurationEvent> l2 =
                EasyMock.createNiceMock(EventListener.class);
        EasyMock.replay(l1, l2);
        File testFile =
                ConfigurationAssert
                        .getTestFile("testCCCombinedChildBuilder.xml");
        builder.configure(new XMLBuilderParametersImpl().setFile(testFile));
        builder.addEventListener(Event.ANY, l1);
        builder.addEventListener(ConfigurationEvent.ANY, l2);
        CombinedConfiguration cc = builder.getConfiguration();
        CombinedConfiguration cc2 =
                (CombinedConfiguration) cc.getConfiguration("subcc");
        Collection<EventListener<? super ConfigurationEvent>> listeners =
                cc2.getEventListeners(ConfigurationEvent.ANY);
        assertTrue("Listener 1 not found", listeners.contains(l1));
        assertTrue("Listener 2 not found", listeners.contains(l2));
        Collection<EventListener<? super Event>> eventListeners =
                cc2.getEventListeners(Event.ANY);
        assertEquals("Wrong number of event listeners", 1,
                eventListeners.size());
        assertTrue("Wrong listener", eventListeners.contains(l1));
    }

    /**
     * Tests whether custom builder providers are inherited to child combined
     * configuration builder providers.
     */
    @Test
    public void testConfigurationBuilderProviderInheritCustomProviders()
            throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(ConfigurationAssert
                        .getTestFile("testCCCustomProvider.xml")));
        builder.getConfiguration();
        CombinedBuilderParametersImpl ccparams =
                new CombinedBuilderParametersImpl();
        builder.initChildBuilderParameters(ccparams);
        assertNotNull("Custom provider not found",
                ccparams.providerForTag("test"));
    }

    /**
     * Tests whether the base path can be inherited to child combined
     * configuration builders.
     */
    @Test
    public void testConfigurationBuilderProviderInheritBasePath()
            throws ConfigurationException
    {
        File envFile =
                ConfigurationAssert.getTestFile("testCCEnvProperties.xml");
        String basePath = ConfigurationAssert.OUT_DIR.getAbsolutePath();
        builder.configure(new CombinedBuilderParametersImpl().setBasePath(
                basePath).setDefinitionBuilderParameters(
                createParameters().setFile(envFile)));
        builder.getConfiguration();
        CombinedBuilderParametersImpl params =
                new CombinedBuilderParametersImpl();
        builder.initChildBuilderParameters(params);
        assertEquals("Base path not set", basePath, params.getBasePath());
    }

    /**
     * Tests whether default child properties in the combined builder's
     * configuration are inherited by child parameter objects.
     */
    @Test
    public void testInitChildBuilderParametersDefaultChildProperties()
            throws ConfigurationException
    {
        final Long defRefresh = 60000L;
        final Long xmlRefresh = 30000L;
        builder.configure(parameters
                .combined()
                .setDefinitionBuilderParameters(
                        parameters.fileBased().setFile(TEST_FILE))
                .registerChildDefaultsHandler(
                        FileBasedBuilderProperties.class,
                        new CopyObjectDefaultHandler(
                                new FileBasedBuilderParametersImpl()
                                        .setReloadingRefreshDelay(defRefresh)
                                        .setThrowExceptionOnMissing(true)))
                .registerChildDefaultsHandler(
                        XMLBuilderProperties.class,
                        new CopyObjectDefaultHandler(
                                new XMLBuilderParametersImpl()
                                        .setValidating(false)
                                        .setExpressionEngine(
                                                new XPathExpressionEngine())
                                        .setReloadingRefreshDelay(xmlRefresh))));
        builder.getConfiguration();
        XMLBuilderParametersImpl params = new XMLBuilderParametersImpl();
        builder.initChildBuilderParameters(params);
        assertTrue(
                "Wrong expression engine",
                params.getParameters().get("expressionEngine") instanceof XPathExpressionEngine);
        assertEquals("Validating flag not set", Boolean.FALSE, params
                .getParameters().get("validating"));
        assertEquals("Wrong XML refresh", xmlRefresh,
                params.getReloadingRefreshDelay());
        assertEquals("Basic flag not set", Boolean.TRUE, params.getParameters()
                .get("throwExceptionOnMissing"));

        PropertiesBuilderParametersImpl params2 =
                new PropertiesBuilderParametersImpl();
        builder.initChildBuilderParameters(params2);
        assertEquals("Wrong default refresh", defRefresh,
                params2.getReloadingRefreshDelay());
    }

    /**
     * Tests whether a Lookup object can be declared in the definition
     * configuration.
     */
    @Test
    public void testCustomLookup() throws ConfigurationException
    {
        File testFile = ConfigurationAssert.getTestFile("testCCLookup.xml");
        builder.configure(createParameters()
                .setFile(testFile));
        CombinedConfiguration cc = builder.getConfiguration();
        assertTrue("Lookup not registered in CC", cc.getInterpolator()
                .getLookups().containsKey("test"));
        Configuration xmlConf = cc.getConfiguration("xml");
        assertTrue("Lookup not registered in sub config", xmlConf
                .getInterpolator().getLookups().containsKey("test"));
    }

    /**
     * Tests whether variable substitution works across multiple child
     * configurations and also in the definition configuration.
     */
    @Test
    public void testInterpolationOverMultipleSources()
            throws ConfigurationException
    {
        File testFile =
                ConfigurationAssert.getTestFile("testInterpolationBuilder.xml");
        builder.configure(createParameters().setFile(testFile));
        CombinedConfiguration combConfig = builder.getConfiguration();
        assertEquals("Wrong value", "abc-product",
                combConfig.getString("products.product.desc"));
        XMLConfiguration xmlConfig =
                (XMLConfiguration) combConfig.getConfiguration("test");
        assertEquals("Wrong value from XML config", "abc-product",
                xmlConfig.getString("products/product/desc"));
        HierarchicalConfiguration<ImmutableNode> subConfig =
                xmlConfig
                        .configurationAt("products/product[@name='abc']", true);
        assertEquals("Wrong value from sub config", "abc-product",
                subConfig.getString("desc"));
    }

    /**
     * Tests whether all child builders can be obtained.
     */
    @Test
    public void testGetChildBuilders() throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        builder.getConfiguration();
        Collection<ConfigurationBuilder<? extends Configuration>> childBuilders =
                builder.getChildBuilders();
        assertEquals("Wrong number of child builders", 3, childBuilders.size());
    }

    /**
     * Loads a test file which includes a MultiFileConfigurationBuilder
     * declaration and returns the resulting configuration.
     *
     * @param fileName the name of the file to be loaded
     * @return the resulting combined configuration
     * @throws ConfigurationException if an error occurs
     */
    private CombinedConfiguration createMultiFileConfig(String fileName)
            throws ConfigurationException
    {
        File testFile = ConfigurationAssert.getTestFile(fileName);
        builder.configure(createParameters()
                .setFile(testFile));
        CombinedConfiguration config = builder.getConfiguration();
        assertTrue("Incorrect result configuration",
                config instanceof DynamicCombinedConfiguration);
        return config;
    }

    /**
     * Tests whether a MultiFileConfigurationBuilder can be integrated into a
     * combined configuration definition.
     */
    @Test
    public void testMultiTenentConfiguration() throws ConfigurationException
    {
        CombinedConfiguration config = createMultiFileConfig("testCCMultiTenent.xml");
        checkMultiFile("1001", config, 15);
        checkMultiFile("1002", config, 25);
        checkMultiFile("1003", config, 35);
        checkMultiFile("1004", config, 50);
    }

    /**
     * Tests whether a configuration created by a MultiFileConfigurationBuilder
     * has been initialized correctly.
     */
    @Test
    public void testMultiTenentConfigurationProperties()
            throws ConfigurationException
    {
        CombinedConfiguration config = createMultiFileConfig("testCCMultiTenent.xml");
        switchToMultiFile("1001");
        HierarchicalConfiguration<?> multiConf =
                (HierarchicalConfiguration<?>) config
                        .getConfiguration("clientConfig");
        assertTrue(
                "Expression engine not configured",
                multiConf.getExpressionEngine() instanceof XPathExpressionEngine);
        assertEquals("Wrong bg color", "#808080",
                config.getString("colors.background"));
        assertEquals("Wrong text color", "#000000",
                multiConf.getString("colors/text"));
    }

    /**
     * Helper method for testing whether properties of a MultiFileConfiguration
     * can be obtained.
     *
     * @param key the key of the file to be accessed
     * @param config the configuration to check
     * @param rows the expected value of the test property
     */
    private void checkMultiFile(String key, CombinedConfiguration config,
            int rows)
    {
        switchToMultiFile(key);
        assertEquals("Wrong property value", rows, config.getInt("rowsPerPage"));
    }

    /**
     * Sets the system property which selects a specific file managed by a
     * MultiFileConfigurationBuilder.
     *
     * @param key the key to select the desired file
     */
    private static void switchToMultiFile(String key)
    {
        System.setProperty(MULTI_FILE_PROPERTY, key);
    }

    /**
     * Tests whether reloading support works for MultiFileConfigurationBuilder.
     */
    @Test
    public void testMultiTenentConfigurationReloading()
            throws ConfigurationException, InterruptedException
    {
        CombinedConfiguration config =
                createMultiFileConfig("testCCMultiTenentReloading.xml");
        File outFile =
                ConfigurationAssert.getOutFile("MultiFileReloadingTest.xml");
        switchToMultiFile(outFile.getAbsolutePath());
        XMLConfiguration reloadConfig = new XMLConfiguration();
        FileHandler handler = new FileHandler(reloadConfig);
        handler.setFile(outFile);
        final String key = "test.reload";
        reloadConfig.setProperty(key, "no");
        handler.save();
        try
        {
            assertEquals("Wrong property", "no", config.getString(key));
            ConfigurationBuilder<? extends Configuration> childBuilder =
                    builder.getNamedBuilder("clientConfig");
            assertTrue("Not a reloading builder",
                    childBuilder instanceof ReloadingControllerSupport);
            ReloadingController ctrl =
                    ((ReloadingControllerSupport) childBuilder)
                            .getReloadingController();
            ctrl.checkForReloading(null); // initialize reloading
            BuilderEventListenerImpl l = new BuilderEventListenerImpl();
            childBuilder.addEventListener(ConfigurationBuilderEvent.RESET, l);
            reloadConfig.setProperty(key, "yes");
            handler.save();

            int attempts = 10;
            boolean changeDetected;
            do
            {
                changeDetected = ctrl.checkForReloading(null);
                if (!changeDetected)
                {
                    Thread.sleep(1000);
                    handler.save(outFile);
                }
            } while (!changeDetected && --attempts > 0);
            assertTrue("No change detected", changeDetected);
            assertEquals("Wrong updated property", "yes", builder
                    .getConfiguration().getString(key));
            ConfigurationBuilderEvent event = l.nextEvent(ConfigurationBuilderEvent.RESET);
            l.assertNoMoreEvents();
            BasicConfigurationBuilder<? extends Configuration> multiBuilder =
                    (BasicConfigurationBuilder<? extends Configuration>) event.getSource();
            childBuilder.removeEventListener(ConfigurationBuilderEvent.RESET, l);
            multiBuilder.resetResult();
            l.assertNoMoreEvents();
        }
        finally
        {
            assertTrue("Output file could not be deleted", outFile.delete());
        }
    }

    /**
     * Tests that the combined configuration has been fully constructed
     * (including its root node) when it is returned from the builder.
     */
    @Test
    public void testRootNodeInitializedAfterCreation()
            throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        CombinedConfiguration cc = builder.getConfiguration();
        assertNotNull("Root node not initialized", cc.getNodeModel()
                .getNodeHandler().getRootNode());
    }

    /**
     * Tests whether a newly created instance can be read concurrently without a
     * special synchronizer.
     */
    @Test
    public void testConcurrentReadAccessWithoutSynchronizer()
            throws ConfigurationException
    {
        builder.configure(createParameters()
                .setFile(TEST_FILE));
        CombinedConfiguration config = builder.getConfiguration();
        final int threadCount = 32;
        CountDownLatch startLatch = new CountDownLatch(1);
        ReadThread[] threads = new ReadThread[threadCount];
        for (int i = 0; i < threadCount; i++)
        {
            threads[i] = new ReadThread(config, startLatch);
            threads[i].start();
        }

        startLatch.countDown();
        for (ReadThread t : threads)
        {
            t.verify();
        }
    }

    /**
     * A test builder provider implementation for testing whether providers can
     * be defined in the definition file.
     */
    public static class BuilderProviderTestImpl implements
            ConfigurationBuilderProvider
    {
        /** The test property key of the configuration to be created. */
        private String propertyKey;

        public String getPropertyKey()
        {
            return propertyKey;
        }

        public void setPropertyKey(String propertyKey)
        {
            this.propertyKey = propertyKey;
        }

        @Override
        public ConfigurationBuilder<? extends Configuration> getConfigurationBuilder(
                ConfigurationDeclaration decl) throws ConfigurationException
        {
            BaseHierarchicalConfiguration config =
                    new BaseHierarchicalConfiguration();
            config.addProperty(getPropertyKey(), Boolean.TRUE);
            return new ConstantConfigurationBuilder(config);
        }
    }

    /**
     * A test builder class which always returns the same configuration.
     */
    private static class ConstantConfigurationBuilder extends
            BasicConfigurationBuilder<BaseHierarchicalConfiguration>
    {
        private final BaseHierarchicalConfiguration configuration;

        public ConstantConfigurationBuilder(BaseHierarchicalConfiguration conf)
        {
            super(BaseHierarchicalConfiguration.class);
            configuration = conf;
        }

        @Override
        public BaseHierarchicalConfiguration getConfiguration()
                throws ConfigurationException
        {
            return configuration;
        }
    }

    /**
     * A specialized entity resolver implementation for testing whether
     * properties of a catalog resolver are correctly set.
     */
    public static class EntityResolverWithPropertiesTestImpl extends
            CatalogResolver
    {
        /** The base directory. */
        private String baseDirectory;

        /** The file system. */
        private FileSystem fileSystem;

        /** The ConfigurationInterpolator. */
        private ConfigurationInterpolator interpolator;

        public FileSystem getFileSystem()
        {
            return fileSystem;
        }

        @Override
        public void setFileSystem(FileSystem fileSystem)
        {
            super.setFileSystem(fileSystem);
            this.fileSystem = fileSystem;
        }

        public String getBaseDir()
        {
            return baseDirectory;
        }

        @Override
        public void setBaseDir(String baseDir)
        {
            super.setBaseDir(baseDir);
            baseDirectory = baseDir;
        }

        public ConfigurationInterpolator getInterpolator()
        {
            return interpolator;
        }

        @Override
        public void setInterpolator(ConfigurationInterpolator interpolator)
        {
            super.setInterpolator(interpolator);
            this.interpolator = interpolator;
        }
    }

    /**
     * A test file system implementation for testing whether a custom file
     * system class can be specified in the configuration definition file.
     */
    public static class FileSystemTestImpl extends DefaultFileSystem
    {
    }

    /**
     * A test combined configuration class for testing whether a specific result
     * configuration class can be declared in the definition configuration.
     */
    public static class CombinedConfigurationTestImpl extends
            CombinedConfiguration
    {
    }

    /**
     * A custom Lookup implementation for testing whether lookups can be defined
     * in the definition configuration. This lookup supports some variables
     * referencing test files.
     */
    public static class TestLookup implements Lookup
    {
        private final Map<String, String> map = new HashMap<String, String>();

        public TestLookup()
        {
            map.put("test_file_xml", "test.xml");
            map.put("test_file_combine", "testcombine1.xml");
            map.put("test_key", "test.value");
        }

        @Override
        public String lookup(String key)
        {
            return map.get(key);
        }
    }

    /**
     * A thread class for testing concurrent read access to a newly created
     * configuration.
     */
    private static class ReadThread extends Thread
    {
        /** The configuration to access. */
        private final CombinedConfiguration config;

        /** The start latch. */
        private final CountDownLatch startLatch;

        /** The value read from the configuration. */
        private Boolean value;

        public ReadThread(CombinedConfiguration cc, CountDownLatch latch)
        {
            config = cc;
            startLatch = latch;
        }

        @Override
        public void run()
        {
            try
            {
                startLatch.await();
                value = config.getBoolean("configuration.loaded");
            }
            catch (InterruptedException iex)
            {
                // ignore
            }
        }

        /**
         * Verifies that the correct value was read.
         */
        public void verify()
        {
            try
            {
                join();
            }
            catch (InterruptedException iex)
            {
                fail("Waiting was interrupted: " + iex);
            }
            assertEquals("Wrong value read", Boolean.TRUE, value);
        }
    }
}
