blob: 28a68438234626f0beb8a6deb4fb7214e213946c [file] [log] [blame]
/*
* 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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
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.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.configuration2.SynchronizerTestImpl.Methods;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.event.ConfigurationEvent;
import org.apache.commons.configuration2.event.EventListener;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.configuration2.sync.LockMode;
import org.apache.commons.configuration2.sync.ReadWriteSynchronizer;
import org.apache.commons.configuration2.sync.Synchronizer;
import org.apache.commons.configuration2.tree.DefaultExpressionEngine;
import org.apache.commons.configuration2.tree.DefaultExpressionEngineSymbols;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.commons.configuration2.tree.NodeCombiner;
import org.apache.commons.configuration2.tree.NodeModel;
import org.apache.commons.configuration2.tree.OverrideCombiner;
import org.apache.commons.configuration2.tree.UnionCombiner;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
/**
* Test class for CombinedConfiguration.
*
* @version $Id$
*/
public class TestCombinedConfiguration
{
/** Constant for the name of a sub configuration. */
private static final String TEST_NAME = "SUBCONFIG";
/** Constant for a test key. */
private static final String TEST_KEY = "test.value";
/** Constant for a key to be used for a concurrent test. */
private static final String KEY_CONCURRENT = "concurrent.access.test";
/** Constant for the name of the first child configuration.*/
private static final String CHILD1 = TEST_NAME + "1";
/** Constant for the name of the second child configuration.*/
private static final String CHILD2 = TEST_NAME + "2";
/** Constant for the key for a sub configuration. */
private static final String SUB_KEY = "test.sub.config";
/** Helper object for managing temporary files. */
@Rule
public TemporaryFolder folder = new TemporaryFolder();
/** The configuration to be tested. */
private CombinedConfiguration config;
/** The test event listener. */
private CombinedListener listener;
@Before
public void setUp() throws Exception
{
config = new CombinedConfiguration();
listener = new CombinedListener();
config.addEventListener(ConfigurationEvent.ANY, listener);
}
/**
* Tests accessing a newly created combined configuration.
*/
@Test
public void testInit()
{
assertEquals("Already configurations contained", 0, config
.getNumberOfConfigurations());
assertTrue("Set of names is not empty", config.getConfigurationNames()
.isEmpty());
assertTrue("Wrong node combiner",
config.getNodeCombiner() instanceof UnionCombiner);
assertNull("Test config was found", config.getConfiguration(TEST_NAME));
}
/**
* Tests adding a configuration (without further information).
*/
@Test
public void testAddConfiguration()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
checkAddConfig(c);
assertEquals("Wrong number of configs", 1, config
.getNumberOfConfigurations());
assertTrue("Name list is not empty", config.getConfigurationNames()
.isEmpty());
assertSame("Added config not found", c, config.getConfiguration(0));
assertTrue("Wrong property value", config.getBoolean(TEST_KEY));
listener.checkEvent(1, 0);
}
/**
* Tests adding a configuration with a name.
*/
@Test
public void testAddConfigurationWithName()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
checkAddConfig(c);
assertEquals("Wrong number of configs", 1, config
.getNumberOfConfigurations());
assertSame("Added config not found", c, config.getConfiguration(0));
assertSame("Added config not found by name", c, config
.getConfiguration(TEST_NAME));
Set<String> names = config.getConfigurationNames();
assertEquals("Wrong number of config names", 1, names.size());
assertTrue("Name not found", names.contains(TEST_NAME));
assertTrue("Wrong property value", config.getBoolean(TEST_KEY));
listener.checkEvent(1, 0);
}
/**
* Tests adding a configuration with a name when this name already exists.
* This should cause an exception.
*/
@Test(expected = ConfigurationRuntimeException.class)
public void testAddConfigurationWithNameTwice()
{
config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
config.addConfiguration(setUpTestConfiguration(), TEST_NAME,
"prefix");
}
/**
* Tests adding a configuration and specifying an at position.
*/
@Test
public void testAddConfigurationAt()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, null, "my");
checkAddConfig(c);
assertTrue("Wrong property value", config.getBoolean("my." + TEST_KEY));
}
/**
* Tests adding a configuration with a complex at position. Here the at path
* contains a dot, which must be escaped.
*/
@Test
public void testAddConfigurationComplexAt()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, null, "This..is.a.complex");
checkAddConfig(c);
assertTrue("Wrong property value", config
.getBoolean("This..is.a.complex." + TEST_KEY));
}
/**
* Checks if a configuration was correctly added to the combined config.
*
* @param c the config to check
*/
private void checkAddConfig(AbstractConfiguration c)
{
Collection<EventListener<? super ConfigurationEvent>> listeners =
c.getEventListeners(ConfigurationEvent.ANY);
assertEquals("Wrong number of configuration listeners", 1, listeners
.size());
assertTrue("Combined config is no listener", listeners.contains(config));
}
/**
* Tests adding a null configuration. This should cause an exception to be
* thrown.
*/
@Test(expected = IllegalArgumentException.class)
public void testAddNullConfiguration()
{
config.addConfiguration(null);
}
/**
* Tests accessing properties if no configurations have been added.
*/
@Test
public void testAccessPropertyEmpty()
{
assertFalse("Found a key", config.containsKey(TEST_KEY));
assertNull("Key has a value", config.getString("test.comment"));
assertTrue("Config is not empty", config.isEmpty());
}
/**
* Tests accessing properties if multiple configurations have been added.
*/
@Test
public void testAccessPropertyMulti()
{
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), null, "prefix1");
config.addConfiguration(setUpTestConfiguration(), null, "prefix2");
assertTrue("Prop1 not found", config.getBoolean(TEST_KEY));
assertTrue("Prop 2 not found", config.getBoolean("prefix1." + TEST_KEY));
assertTrue("Prop 3 not found", config.getBoolean("prefix2." + TEST_KEY));
assertFalse("Configuration is empty", config.isEmpty());
listener.checkEvent(3, 0);
}
/**
* Tests removing a configuration.
*/
@Test
public void testRemoveConfiguration()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
checkAddConfig(c);
assertTrue("Config could not be removed", config.removeConfiguration(c));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration by index.
*/
@Test
public void testRemoveConfigurationAt()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
assertSame("Wrong config removed", c, config.removeConfigurationAt(0));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration by name.
*/
@Test
public void testRemoveConfigurationByName()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
assertSame("Wrong config removed", c, config
.removeConfiguration(TEST_NAME));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration with a name.
*/
@Test
public void testRemoveNamedConfiguration()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
config.removeConfiguration(c);
checkRemoveConfig(c);
}
/**
* Tests removing a named configuration by index.
*/
@Test
public void testRemoveNamedConfigurationAt()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
assertSame("Wrong config removed", c, config.removeConfigurationAt(0));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration that was not added prior.
*/
@Test
public void testRemoveNonContainedConfiguration()
{
assertFalse("Could remove non contained config", config
.removeConfiguration(setUpTestConfiguration()));
listener.checkEvent(0, 0);
}
/**
* Tests removing a configuration by name, which is not contained.
*/
@Test
public void testRemoveConfigurationByUnknownName()
{
assertNull("Could remove configuration by unknown name", config
.removeConfiguration("unknownName"));
listener.checkEvent(0, 0);
}
/**
* Tests whether a configuration was completely removed.
*
* @param c the removed configuration
*/
private void checkRemoveConfig(AbstractConfiguration c)
{
assertTrue("Listener was not removed",
c.getEventListeners(ConfigurationEvent.ANY).isEmpty());
assertEquals("Wrong number of contained configs", 0, config
.getNumberOfConfigurations());
assertTrue("Name was not removed", config.getConfigurationNames()
.isEmpty());
listener.checkEvent(2, 0);
}
/**
* Tests if an update of a contained configuration leeds to an invalidation
* of the combined configuration.
*/
@Test
public void testUpdateContainedConfiguration()
{
AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
c.addProperty("test.otherTest", "yes");
assertEquals("New property not found", "yes", config
.getString("test.otherTest"));
listener.checkEvent(2, 0);
}
/**
* Tests if setting a node combiner causes an invalidation.
*/
@Test
public void testSetNodeCombiner()
{
NodeCombiner combiner = new UnionCombiner();
config.setNodeCombiner(combiner);
assertSame("Node combiner was not set", combiner, config
.getNodeCombiner());
listener.checkEvent(1, 0);
}
/**
* Tests setting a null node combiner. This should cause an exception.
*/
@Test(expected = IllegalArgumentException.class)
public void testSetNullNodeCombiner()
{
config.setNodeCombiner(null);
}
/**
* Tests cloning a combined configuration.
*/
@Test
public void testClone()
{
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
config.addConfiguration(new PropertiesConfiguration(), "props");
CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
assertNotNull("No root node", cc2.getModel().getNodeHandler()
.getRootNode());
assertEquals("Wrong number of contained configurations", config
.getNumberOfConfigurations(), cc2.getNumberOfConfigurations());
assertSame("Wrong node combiner", config.getNodeCombiner(), cc2
.getNodeCombiner());
assertEquals("Wrong number of names", config.getConfigurationNames()
.size(), cc2.getConfigurationNames().size());
assertTrue(
"Found duplicate event listeners",
Collections.disjoint(
cc2.getEventListeners(ConfigurationEvent.ANY),
config.getEventListeners(ConfigurationEvent.ANY)));
StrictConfigurationComparator comp = new StrictConfigurationComparator();
for (int i = 0; i < config.getNumberOfConfigurations(); i++)
{
assertNotSame("Configuration at " + i + " was not cloned", config
.getConfiguration(i), cc2.getConfiguration(i));
assertEquals("Wrong config class at " + i, config.getConfiguration(
i).getClass(), cc2.getConfiguration(i).getClass());
assertTrue("Configs not equal at " + i, comp.compare(config
.getConfiguration(i), cc2.getConfiguration(i)));
}
assertTrue("Combined configs not equal", comp.compare(config, cc2));
}
/**
* Tests if the cloned configuration is decoupled from the original.
*/
@Test
public void testCloneModify()
{
config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
assertTrue("Name is missing", cc2.getConfigurationNames().contains(
TEST_NAME));
cc2.removeConfiguration(TEST_NAME);
assertFalse("Names in original changed", config.getConfigurationNames()
.isEmpty());
}
/**
* Tests clearing a combined configuration. This should remove all contained
* configurations.
*/
@Test
public void testClear()
{
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "test");
config.addConfiguration(setUpTestConfiguration());
config.clear();
assertEquals("Still configs contained", 0, config
.getNumberOfConfigurations());
assertTrue("Still names contained", config.getConfigurationNames()
.isEmpty());
assertTrue("Config is not empty", config.isEmpty());
listener.checkEvent(3, 2);
}
/**
* Tests whether the combined configuration removes itself as change
* listener from the child configurations on a clear operation. This test is
* related to CONFIGURATION-572.
*/
@Test
public void testClearRemoveChildListener()
{
AbstractConfiguration child = setUpTestConfiguration();
config.addConfiguration(child);
config.clear();
for (EventListener<?> listener : child
.getEventListeners(ConfigurationEvent.ANY))
{
assertNotEquals("Still registered", config, listener);
}
}
/**
* Prepares a test of the getSource() method.
*/
private void setUpSourceTest()
{
BaseHierarchicalConfiguration c1 = new BaseHierarchicalConfiguration();
PropertiesConfiguration c2 = new PropertiesConfiguration();
c1.addProperty(TEST_KEY, TEST_NAME);
c2.addProperty("another.key", "test");
config.addConfiguration(c1, CHILD1);
config.addConfiguration(c2, CHILD2);
}
/**
* Tests the gestSource() method when the source property is defined in a
* hierarchical configuration.
*/
@Test
public void testGetSourceHierarchical()
{
setUpSourceTest();
assertEquals("Wrong source configuration", config
.getConfiguration(CHILD1), config.getSource(TEST_KEY));
}
/**
* Tests whether the source configuration can be detected for non
* hierarchical configurations.
*/
@Test
public void testGetSourceNonHierarchical()
{
setUpSourceTest();
assertEquals("Wrong source configuration", config
.getConfiguration(CHILD2), config.getSource("another.key"));
}
/**
* Tests the getSource() method when the passed in key is not contained.
* Result should be null in this case.
*/
@Test
public void testGetSourceUnknown()
{
setUpSourceTest();
assertNull("Wrong result for unknown key", config
.getSource("an.unknown.key"));
}
/**
* Tests the getSource() method when a null key is passed in. This should
* cause an exception.
*/
@Test(expected = IllegalArgumentException.class)
public void testGetSourceNull()
{
config.getSource(null);
}
/**
* Tests the getSource() method when the passed in key belongs to the
* combined configuration itself.
*/
@Test
public void testGetSourceCombined()
{
setUpSourceTest();
final String key = "yet.another.key";
config.addProperty(key, Boolean.TRUE);
assertEquals("Wrong source for key", config, config.getSource(key));
}
/**
* Tests the getSource() method when the passed in key refers to multiple
* values, which are all defined in the same source configuration.
*/
@Test
public void testGetSourceMulti()
{
setUpSourceTest();
final String key = "list.key";
config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
assertEquals("Wrong source for multi-value property", config
.getConfiguration(CHILD1), config.getSource(key));
}
/**
* Tests the getSource() method when the passed in key refers to multiple
* values defined by different sources. This should cause an exception.
*/
@Test(expected = IllegalArgumentException.class)
public void testGetSourceMultiSources()
{
setUpSourceTest();
final String key = "list.key";
config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
config.getConfiguration(CHILD2).addProperty(key, "a,b,c");
config.getSource(key);
}
/**
* Tests getSource() if a child configuration is again a combined configuration.
*/
@Test
public void testGetSourceWithCombinedChildConfiguration()
{
setUpSourceTest();
CombinedConfiguration cc = new CombinedConfiguration();
cc.addConfiguration(config);
assertEquals("Wrong source", config, cc.getSource(TEST_KEY));
}
/**
* Tests whether multiple sources of a key can be retrieved.
*/
@Test
public void testGetSourcesMultiSources()
{
setUpSourceTest();
final String key = "list.key";
config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
config.getConfiguration(CHILD2).addProperty(key, "a,b,c");
Set<Configuration> sources = config.getSources(key);
assertEquals("Wrong number of sources", 2, sources.size());
assertTrue("Source 1 not found",
sources.contains(config.getConfiguration(CHILD1)));
assertTrue("Source 2 not found",
sources.contains(config.getConfiguration(CHILD2)));
}
/**
* Tests getSources() for a non existing key.
*/
@Test
public void testGetSourcesUnknownKey()
{
setUpSourceTest();
assertTrue("Got sources", config.getSources("non.existing,key")
.isEmpty());
}
/**
* Tests whether escaped list delimiters are treated correctly.
*/
@Test
public void testEscapeListDelimiters()
{
PropertiesConfiguration sub = new PropertiesConfiguration();
sub.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
sub.addProperty("test.pi", "3\\,1415");
config.addConfiguration(sub);
assertEquals("Wrong value", "3,1415", config.getString("test.pi"));
}
/**
* Tests whether only a single invalidate event is fired for a change. This
* test is related to CONFIGURATION-315.
*/
@Test
public void testInvalidateEventBeforeAndAfterChange()
{
ConfigurationEvent event =
new ConfigurationEvent(config, ConfigurationEvent.ANY, null, null, true);
config.onEvent(event);
assertEquals("No invalidate event fired", 1, listener.invalidateEvents);
event = new ConfigurationEvent(config, ConfigurationEvent.ANY, null, null, false);
config.onEvent(event);
assertEquals("Another invalidate event fired", 1,
listener.invalidateEvents);
}
/**
* Tests using a conversion expression engine for child configurations with
* strange keys. This test is related to CONFIGURATION-336.
*/
@Test
public void testConversionExpressionEngine()
{
PropertiesConfiguration child = new PropertiesConfiguration();
child.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
child.addProperty("test(a)", "1,2,3");
config.addConfiguration(child);
DefaultExpressionEngine engineQuery =
new DefaultExpressionEngine(
new DefaultExpressionEngineSymbols.Builder(
DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS)
.setIndexStart("<").setIndexEnd(">").create());
config.setExpressionEngine(engineQuery);
DefaultExpressionEngine engineConvert =
new DefaultExpressionEngine(
new DefaultExpressionEngineSymbols.Builder(
DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS)
.setIndexStart("[").setIndexEnd("]").create());
config.setConversionExpressionEngine(engineConvert);
assertEquals("Wrong property 1", "1", config.getString("test(a)<0>"));
assertEquals("Wrong property 2", "2", config.getString("test(a)<1>"));
assertEquals("Wrong property 3", "3", config.getString("test(a)<2>"));
}
@Test
public void testGetConfigurations() throws Exception
{
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
AbstractConfiguration pc = new PropertiesConfiguration();
config.addConfiguration(pc, "props");
List<Configuration> list = config.getConfigurations();
assertNotNull("No list of configurations returned", list);
assertTrue("Incorrect number of configurations", list.size() == 3);
Configuration c = list.get(2);
assertTrue("Incorrect configuration", c == pc);
}
@Test
public void testGetConfigurationNameList() throws Exception
{
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
AbstractConfiguration pc = new PropertiesConfiguration();
config.addConfiguration(pc, "props");
List<String> list = config.getConfigurationNameList();
assertNotNull("No list of configurations returned", list);
assertTrue("Incorrect number of configurations", list.size() == 3);
String name = list.get(1);
assertNotNull("No name returned", name);
assertTrue("Incorrect configuration name", TEST_NAME.equals(name));
}
/**
* Tests whether a combined configuration can be copied to an XML
* configuration. This test is related to CONFIGURATION-445.
*/
@Test
public void testCombinedCopyToXML() throws ConfigurationException
{
XMLConfiguration x1 = new XMLConfiguration();
x1.addProperty("key1", "value1");
x1.addProperty("key1[@override]", "USER1");
x1.addProperty("key2", "value2");
x1.addProperty("key2[@override]", "USER2");
XMLConfiguration x2 = new XMLConfiguration();
x2.addProperty("key2", "value2.2");
x2.addProperty("key2[@override]", "USER2");
config.setNodeCombiner(new OverrideCombiner());
config.addConfiguration(x2);
config.addConfiguration(x1);
XMLConfiguration x3 = new XMLConfiguration(config);
assertEquals("Wrong element value", "value2.2", x3.getString("key2"));
assertEquals("Wrong attribute value", "USER2",
x3.getString("key2[@override]"));
StringWriter w = new StringWriter();
new FileHandler(x3).save(w);
String s = w.toString();
x3 = new XMLConfiguration();
new FileHandler(x3).load(new StringReader(s));
assertEquals("Wrong element value after load", "value2.2",
x3.getString("key2"));
assertEquals("Wrong attribute value after load", "USER2",
x3.getString("key2[@override]"));
}
/**
* Prepares a test for synchronization. This method installs a test
* synchronizer and adds some test configurations.
*
* @return the test synchronizer
*/
private SynchronizerTestImpl setUpSynchronizerTest()
{
setUpSourceTest();
SynchronizerTestImpl sync = new SynchronizerTestImpl();
config.setSynchronizer(sync);
return sync;
}
/**
* Tests whether adding a new configuration is synchronized.
*/
@Test
public void testAddConfigurationSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
config.addConfiguration(new BaseHierarchicalConfiguration());
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
checkCombinedRootNotConstructed();
}
/**
* Tests whether setNodeCombiner() is correctly synchronized.
*/
@Test
public void testSetNodeCombinerSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
config.setNodeCombiner(new UnionCombiner());
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
checkCombinedRootNotConstructed();
}
/**
* Tests whether getNodeCombiner() is correctly synchronized.
*/
@Test
public void testGetNodeCombinerSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull("No node combiner", config.getNodeCombiner());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether access to a configuration by index is correctly
* synchronized.
*/
@Test
public void testGetConfigurationByIdxSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull("No configuration", config.getConfiguration(0));
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether access to a configuration by name is correctly
* synchronized.
*/
@Test
public void testGetConfigurationByNameSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull("No configuration", config.getConfiguration(CHILD1));
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether querying the name set of child configurations is
* synchronized.
*/
@Test
public void testGetConfigurationNamesSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertFalse("No child names", config.getConfigurationNames().isEmpty());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether querying the name list of child configurations is
* synchronized.
*/
@Test
public void testGetConfigurationNameListSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertFalse("No child names", config.getConfigurationNameList()
.isEmpty());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Helper method for testing that the combined root node has not yet been
* constructed.
*/
private void checkCombinedRootNotConstructed()
{
assertTrue("Root node was constructed", config.getModel()
.getNodeHandler().getRootNode().getChildren().isEmpty());
}
/**
* Tests whether querying the list of child configurations is synchronized.
*/
@Test
public void testGetConfigurationsSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertFalse("No child configurations", config.getConfigurations()
.isEmpty());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether read access to the conversion expression engine is
* synchronized.
*/
@Test
public void testGetConversionExpressionEngineSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNull("Got a conversion engine",
config.getConversionExpressionEngine());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether write access to the conversion expression engine is
* synchronized.
*/
@Test
public void testSetConversionExpressionEngineSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
config.setConversionExpressionEngine(new DefaultExpressionEngine(
DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS));
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
checkCombinedRootNotConstructed();
}
/**
* Tests whether invalidate() performs correct synchronization.
*/
@Test
public void testInvalidateSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
config.invalidate();
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
}
/**
* Tests whether getSource() is correctly synchronized.
*/
@Test
public void testGetSourceSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull("No source found", config.getSource(TEST_KEY));
sync.verifyStart(Methods.BEGIN_READ);
sync.verifyEnd(Methods.END_READ);
}
/**
* Tests whether querying the number of child configurations is
* synchronized.
*/
@Test
public void testGetNumberOfConfigurationsSynchronized()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
assertEquals("Wrong number of configurations", 2,
config.getNumberOfConfigurations());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether cloning of a configuration is correctly synchronized.
*/
@Test
public void testCloneSynchronized()
{
setUpSourceTest();
config.lock(LockMode.READ); // Causes the root node to be constructed
config.unlock(LockMode.READ);
SynchronizerTestImpl sync = new SynchronizerTestImpl();
config.setSynchronizer(sync);
config.clone();
// clone() of base class is wrapped by another read lock
sync.verifyStart(Methods.BEGIN_READ, Methods.BEGIN_READ);
sync.verifyEnd(Methods.END_READ, Methods.END_READ);
}
/**
* Tests whether requested locks are freed correctly if an exception occurs
* while constructing the root node.
*/
@Test
public void testLockHandlingWithExceptionWhenConstructingRootNode()
{
SynchronizerTestImpl sync = setUpSynchronizerTest();
final RuntimeException testEx =
new ConfigurationRuntimeException("Test exception");
BaseHierarchicalConfiguration childEx =
new BaseHierarchicalConfiguration()
{
@Override
public NodeModel<ImmutableNode> getModel() {
throw testEx;
}
};
config.addConfiguration(childEx);
try
{
config.lock(LockMode.READ);
fail("Exception not detected!");
}
catch (Exception ex)
{
assertEquals("Unexpected exception", testEx, ex);
}
// 1 x add configuration, then obtain read lock and create root node
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_READ,
Methods.END_READ, Methods.BEGIN_WRITE, Methods.END_WRITE);
}
/**
* Tests concurrent read and write access on a combined configuration. There
* are multiple reader threads and a single writer thread. It is checked
* that no inconsistencies occur.
*/
@Test
public void testConcurrentAccess() throws ConfigurationException,
InterruptedException
{
// populate the test combined configuration
setUpSourceTest();
XMLConfiguration xmlConf = new XMLConfiguration();
new FileHandler(xmlConf).load(ConfigurationAssert
.getTestFile("test.xml"));
config.addConfiguration(xmlConf);
PropertiesConfiguration propConf = new PropertiesConfiguration();
new FileHandler(propConf).load(ConfigurationAssert
.getTestFile("test.properties"));
for (int i = 0; i < 8; i++)
{
config.addConfiguration(new BaseHierarchicalConfiguration());
}
config.getConfiguration(0).addProperty(KEY_CONCURRENT, TEST_NAME);
// Set a single synchronizer for all involved configurations
Synchronizer sync = new ReadWriteSynchronizer();
config.setSynchronizer(sync);
for (Configuration c : config.getConfigurations())
{
c.setSynchronizer(sync);
}
// setup test threads
final int numberOfReaders = 3;
final int readCount = 5000;
final int writeCount = 3000;
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger errorCount = new AtomicInteger();
Collection<Thread> threads = new ArrayList<Thread>(numberOfReaders + 1);
Thread writeThread =
new WriteThread(config, latch, errorCount, writeCount);
writeThread.start();
threads.add(writeThread);
for (int i = 0; i < numberOfReaders; i++)
{
Thread readThread =
new ReadThread(config, latch, errorCount, readCount);
readThread.start();
threads.add(readThread);
}
// perform test
latch.countDown();
for (Thread t : threads)
{
t.join();
}
assertEquals("Got errors", 0, errorCount.get());
}
/**
* Prepares the test configuration for a test for sub configurations. Some
* child configurations are added.
*
* @return the sub configuration at the test sub key
*/
private AbstractConfiguration setUpSubConfigTest()
{
AbstractConfiguration srcConfig = setUpTestConfiguration();
config.addConfiguration(srcConfig, "source", SUB_KEY);
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), "otherTest",
"other.prefix");
return srcConfig;
}
/**
* Tests whether a sub configuration survives updates of its parent.
*/
@Test
public void testSubConfigurationWithUpdates()
{
AbstractConfiguration srcConfig = setUpSubConfigTest();
HierarchicalConfiguration<ImmutableNode> sub =
config.configurationAt(SUB_KEY, true);
assertTrue("Wrong value before update", sub.getBoolean(TEST_KEY));
srcConfig.setProperty(TEST_KEY, Boolean.FALSE);
assertFalse("Wrong value after update", sub.getBoolean(TEST_KEY));
assertFalse("Wrong value from combined configuration",
config.getBoolean(SUB_KEY + '.' + TEST_KEY));
}
/**
* Checks the configurationsAt() method.
* @param withUpdates flag whether updates are supported
*/
private void checkConfigurationsAt(boolean withUpdates)
{
setUpSubConfigTest();
List<HierarchicalConfiguration<ImmutableNode>> subs =
config.configurationsAt(SUB_KEY, withUpdates);
assertEquals("Wrong number of sub configurations", 1, subs.size());
assertTrue("Wrong value in sub configuration",
subs.get(0).getBoolean(TEST_KEY));
}
/**
* Tests whether sub configurations can be created from a key.
*/
@Test
public void testConfigurationsAt()
{
checkConfigurationsAt(false);
}
/**
* Tests whether sub configurations can be created which are attached.
*/
@Test
public void testConfigurationsAtWithUpdates()
{
checkConfigurationsAt(true);
}
/**
* Helper method for creating a test configuration to be added to the
* combined configuration.
*
* @return the test configuration
*/
private static AbstractConfiguration setUpTestConfiguration()
{
BaseHierarchicalConfiguration config = new BaseHierarchicalConfiguration();
config.addProperty(TEST_KEY, Boolean.TRUE);
config.addProperty("test.comment", "This is a test");
return config;
}
/**
* Test event listener class for checking if the expected invalidate events
* are fired.
*/
private static class CombinedListener implements EventListener<ConfigurationEvent>
{
int invalidateEvents;
int otherEvents;
@Override
public void onEvent(ConfigurationEvent event)
{
if (event.getEventType() == CombinedConfiguration.COMBINED_INVALIDATE)
{
invalidateEvents++;
}
else
{
otherEvents++;
}
}
/**
* Checks if the expected number of events was fired.
*
* @param expectedInvalidate the expected number of invalidate events
* @param expectedOthers the expected number of other events
*/
public void checkEvent(int expectedInvalidate, int expectedOthers)
{
assertEquals("Wrong number of invalidate events",
expectedInvalidate, invalidateEvents);
assertEquals("Wrong number of other events", expectedOthers,
otherEvents);
}
}
/**
* A test thread performing reads on a combined configuration. This thread
* reads a certain property from the configuration. If everything works
* well, this property should have at least one and at most two values.
*/
private static class ReadThread extends Thread
{
/** The configuration to be accessed. */
private final Configuration config;
/** The latch for synchronizing thread start. */
private final CountDownLatch startLatch;
/** A counter for read errors. */
private final AtomicInteger errorCount;
/** The number of reads to be performed. */
private final int numberOfReads;
/**
* Creates a new instance of {@code ReadThread}.
*
* @param readConfig the configuration to be read
* @param latch the latch for synchronizing thread start
* @param errCnt the counter for read errors
* @param readCount the number of reads to be performed
*/
public ReadThread(Configuration readConfig, CountDownLatch latch,
AtomicInteger errCnt, int readCount)
{
config = readConfig;
startLatch = latch;
errorCount = errCnt;
numberOfReads = readCount;
}
/**
* Reads from the test configuration.
*/
@Override
public void run()
{
try
{
startLatch.await();
for (int i = 0; i < numberOfReads; i++)
{
readConfiguration();
}
}
catch (Exception e)
{
errorCount.incrementAndGet();
}
}
/**
* Reads the test property from the associated configuration. Its values
* are checked.
*/
private void readConfiguration()
{
List<Object> values = config.getList(KEY_CONCURRENT);
if (values.size() < 1 || values.size() > 2)
{
errorCount.incrementAndGet();
}
else
{
boolean ok = true;
for (Object value : values)
{
if (!TEST_NAME.equals(value))
{
ok = false;
}
}
if (!ok)
{
errorCount.incrementAndGet();
}
}
}
}
/**
* A test thread performing updates on a test configuration. This thread
* modifies configurations which are children of a combined configuration.
* Each update operation adds a value to one of the child configurations and
* removes it from another one (which contained it before). So if concurrent
* reads are performed, the test property should always have between 1 and 2
* values.
*/
private static class WriteThread extends Thread
{
/** The list with the child configurations. */
private final List<Configuration> testConfigs;
/** The latch for synchronizing thread start. */
private final CountDownLatch startLatch;
/** A counter for errors. */
private final AtomicInteger errorCount;
/** The number of write operations to be performed. */
private final int numberOfWrites;
/** The index of the child configuration containing the test property. */
private int currentChildConfigIdx;
/**
* Creates a new instance of {@code WriteThread}.
*
* @param cc the test combined configuration
* @param latch the latch for synchronizing test start
* @param errCnt a counter for errors
* @param writeCount the number of writes to be performed
*/
public WriteThread(CombinedConfiguration cc, CountDownLatch latch,
AtomicInteger errCnt, int writeCount)
{
testConfigs = cc.getConfigurations();
startLatch = latch;
errorCount = errCnt;
numberOfWrites = writeCount;
}
@Override
public void run()
{
try
{
startLatch.await();
for (int i = 0; i < numberOfWrites; i++)
{
updateConfigurations();
}
}
catch (InterruptedException e)
{
errorCount.incrementAndGet();
}
}
/**
* Performs the update operation.
*/
private void updateConfigurations()
{
int newIdx = (currentChildConfigIdx + 1) % testConfigs.size();
testConfigs.get(newIdx).addProperty(KEY_CONCURRENT, TEST_NAME);
testConfigs.get(currentChildConfigIdx)
.clearProperty(KEY_CONCURRENT);
currentChildConfigIdx = newIdx;
}
}
}