blob: 0888233010c0d7738b4bf9d587564b9d338d6ced [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
*
* https://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.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
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.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test class for CombinedConfiguration.
*/
public class TestCombinedConfiguration {
/**
* Test event listener class for checking if the expected invalidate events are fired.
*/
private static final class CombinedListener implements EventListener<ConfigurationEvent> {
private int invalidateEvents;
private int 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(final int expectedInvalidate, final int expectedOthers) {
assertEquals(expectedInvalidate, invalidateEvents);
assertEquals(expectedOthers, otherEvents);
}
@Override
public void onEvent(final ConfigurationEvent event) {
if (event.getEventType() == CombinedConfiguration.COMBINED_INVALIDATE) {
invalidateEvents++;
} else {
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 final 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(final Configuration readConfig, final CountDownLatch latch, final AtomicInteger errCnt, final int readCount) {
config = readConfig;
startLatch = latch;
errorCount = errCnt;
numberOfReads = readCount;
}
/**
* Reads the test property from the associated configuration. Its values are checked.
*/
private void readConfiguration() {
final List<Object> values = config.getList(KEY_CONCURRENT);
if (values.size() < 1 || values.size() > 2) {
errorCount.incrementAndGet();
} else {
boolean ok = true;
for (final Object value : values) {
if (!TEST_NAME.equals(value)) {
ok = false;
}
}
if (!ok) {
errorCount.incrementAndGet();
}
}
}
/**
* Reads from the test configuration.
*/
@Override
public void run() {
try {
startLatch.await();
for (int i = 0; i < numberOfReads; i++) {
readConfiguration();
}
} catch (final Exception e) {
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 final 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(final CombinedConfiguration cc, final CountDownLatch latch, final AtomicInteger errCnt, final 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 (final InterruptedException e) {
errorCount.incrementAndGet();
}
}
/**
* Performs the update operation.
*/
private void updateConfigurations() {
final int newIdx = (currentChildConfigIdx + 1) % testConfigs.size();
testConfigs.get(newIdx).addProperty(KEY_CONCURRENT, TEST_NAME);
testConfigs.get(currentChildConfigIdx).clearProperty(KEY_CONCURRENT);
currentChildConfigIdx = newIdx;
}
}
/** 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 method for creating a test configuration to be added to the combined configuration.
*
* @return the test configuration
*/
private static AbstractConfiguration setUpTestConfiguration() {
final BaseHierarchicalConfiguration config = new BaseHierarchicalConfiguration();
config.addProperty(TEST_KEY, Boolean.TRUE);
config.addProperty("test.comment", "This is a test");
return config;
}
/** The configuration to be tested. */
private CombinedConfiguration config;
/** The test event listener. */
private CombinedListener listener;
/**
* Checks if a configuration was correctly added to the combined config.
*
* @param c the config to check
*/
private void checkAddConfig(final AbstractConfiguration c) {
final Collection<EventListener<? super ConfigurationEvent>> listeners = c.getEventListeners(ConfigurationEvent.ANY);
assertEquals(1, listeners.size());
assertTrue(listeners.contains(config));
}
/**
* Helper method for testing that the combined root node has not yet been constructed.
*/
private void checkCombinedRootNotConstructed() {
assertTrue(config.getModel().getNodeHandler().getRootNode().getChildren().isEmpty());
}
/**
* Checks the configurationsAt() method.
*
* @param withUpdates flag whether updates are supported
*/
private void checkConfigurationsAt(final boolean withUpdates) {
setUpSubConfigTest();
final List<HierarchicalConfiguration<ImmutableNode>> subs = config.configurationsAt(SUB_KEY, withUpdates);
assertEquals(1, subs.size());
assertTrue(subs.get(0).getBoolean(TEST_KEY));
}
/**
* Tests whether a configuration was completely removed.
*
* @param c the removed configuration
*/
private void checkRemoveConfig(final AbstractConfiguration c) {
assertTrue(c.getEventListeners(ConfigurationEvent.ANY).isEmpty());
assertEquals(0, config.getNumberOfConfigurations());
assertTrue(config.getConfigurationNames().isEmpty());
listener.checkEvent(2, 0);
}
@BeforeEach
public void setUp() throws Exception {
config = new CombinedConfiguration();
listener = new CombinedListener();
config.addEventListener(ConfigurationEvent.ANY, listener);
}
/**
* Prepares a test of the getSource() method.
*/
private void setUpSourceTest() {
final BaseHierarchicalConfiguration c1 = new BaseHierarchicalConfiguration();
final PropertiesConfiguration c2 = new PropertiesConfiguration();
c1.addProperty(TEST_KEY, TEST_NAME);
c2.addProperty("another.key", "test");
config.addConfiguration(c1, CHILD1);
config.addConfiguration(c2, CHILD2);
}
/**
* 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() {
final AbstractConfiguration srcConfig = setUpTestConfiguration();
config.addConfiguration(srcConfig, "source", SUB_KEY);
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), "otherTest", "other.prefix");
return srcConfig;
}
/**
* Prepares a test for synchronization. This method installs a test synchronizer and adds some test configurations.
*
* @return the test synchronizer
*/
private SynchronizerTestImpl setUpSynchronizerTest() {
setUpSourceTest();
final SynchronizerTestImpl sync = new SynchronizerTestImpl();
config.setSynchronizer(sync);
return sync;
}
/**
* Tests accessing properties if no configurations have been added.
*/
@Test
void testAccessPropertyEmpty() {
assertFalse(config.containsKey(TEST_KEY));
assertNull(config.getString("test.comment"));
assertTrue(config.isEmpty());
}
/**
* Tests accessing properties if multiple configurations have been added.
*/
@Test
void testAccessPropertyMulti() {
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), null, "prefix1");
config.addConfiguration(setUpTestConfiguration(), null, "prefix2");
assertTrue(config.getBoolean(TEST_KEY));
assertTrue(config.getBoolean("prefix1." + TEST_KEY));
assertTrue(config.getBoolean("prefix2." + TEST_KEY));
assertFalse(config.isEmpty());
listener.checkEvent(3, 0);
}
/**
* Tests adding a configuration (without further information).
*/
@Test
void testAddConfiguration() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
checkAddConfig(c);
assertEquals(1, config.getNumberOfConfigurations());
assertTrue(config.getConfigurationNames().isEmpty());
assertSame(c, config.getConfiguration(0));
assertTrue(config.getBoolean(TEST_KEY));
listener.checkEvent(1, 0);
}
/**
* Tests adding a configuration and specifying an at position.
*/
@Test
void testAddConfigurationAt() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, null, "my");
checkAddConfig(c);
assertTrue(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
void testAddConfigurationComplexAt() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, null, "This..is.a.complex");
checkAddConfig(c);
assertTrue(config.getBoolean("This..is.a.complex." + TEST_KEY));
}
/**
* Tests whether adding a new configuration is synchronized.
*/
@Test
void testAddConfigurationSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
config.addConfiguration(new BaseHierarchicalConfiguration());
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
checkCombinedRootNotConstructed();
}
/**
* Tests adding a configuration with a name.
*/
@Test
void testAddConfigurationWithName() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
checkAddConfig(c);
assertEquals(1, config.getNumberOfConfigurations());
assertSame(c, config.getConfiguration(0));
assertSame(c, config.getConfiguration(TEST_NAME));
final Set<String> names = config.getConfigurationNames();
assertEquals(1, names.size());
assertTrue(names.contains(TEST_NAME));
assertTrue(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
void testAddConfigurationWithNameTwice() {
config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
final Configuration configuration = setUpTestConfiguration();
assertThrows(ConfigurationRuntimeException.class, () -> config.addConfiguration(configuration, TEST_NAME, "prefix"));
}
/**
* Tests adding a null configuration. This should cause an exception to be thrown.
*/
@Test
void testAddNullConfiguration() {
assertThrows(IllegalArgumentException.class, () -> config.addConfiguration(null));
}
/**
* Tests clearing a combined configuration. This should remove all contained configurations.
*/
@Test
void testClear() {
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "test");
config.addConfiguration(setUpTestConfiguration());
config.clear();
assertEquals(0, config.getNumberOfConfigurations());
assertTrue(config.getConfigurationNames().isEmpty());
assertTrue(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
void testClearRemoveChildListener() {
final AbstractConfiguration child = setUpTestConfiguration();
config.addConfiguration(child);
config.clear();
for (final EventListener<?> listener : child.getEventListeners(ConfigurationEvent.ANY)) {
assertNotEquals(config, listener);
}
}
/**
* Tests cloning a combined configuration.
*/
@Test
void testClone() {
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
config.addConfiguration(new PropertiesConfiguration(), "props");
final CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
assertNotNull(cc2.getModel().getNodeHandler().getRootNode());
assertEquals(config.getNumberOfConfigurations(), cc2.getNumberOfConfigurations());
assertSame(config.getNodeCombiner(), cc2.getNodeCombiner());
assertEquals(config.getConfigurationNames().size(), cc2.getConfigurationNames().size());
assertTrue(Collections.disjoint(cc2.getEventListeners(ConfigurationEvent.ANY), config.getEventListeners(ConfigurationEvent.ANY)));
final StrictConfigurationComparator comp = new StrictConfigurationComparator();
for (int i = 0; i < config.getNumberOfConfigurations(); i++) {
assertNotSame(config.getConfiguration(i), cc2.getConfiguration(i), "Configuration at " + i + " was not cloned");
assertEquals(config.getConfiguration(i).getClass(), cc2.getConfiguration(i).getClass(), "Wrong config class at " + i);
assertTrue(comp.compare(config.getConfiguration(i), cc2.getConfiguration(i)), "Configs not equal at " + i);
}
assertTrue(comp.compare(config, cc2));
}
/**
* Tests if the cloned configuration is decoupled from the original.
*/
@Test
void testCloneModify() {
config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
final CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
assertTrue(cc2.getConfigurationNames().contains(TEST_NAME));
cc2.removeConfiguration(TEST_NAME);
assertFalse(config.getConfigurationNames().isEmpty());
}
/**
* Tests whether cloning of a configuration is correctly synchronized.
*/
@Test
void testCloneSynchronized() {
setUpSourceTest();
config.lock(LockMode.READ); // Causes the root node to be constructed
config.unlock(LockMode.READ);
final 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 a combined configuration can be copied to an XML configuration. This test is related to
* CONFIGURATION-445.
*/
@Test
void testCombinedCopyToXML() throws ConfigurationException {
final XMLConfiguration x1 = new XMLConfiguration();
x1.addProperty("key1", "value1");
x1.addProperty("key1[@override]", "USER1");
x1.addProperty("key2", "value2");
x1.addProperty("key2[@override]", "USER2");
final 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("value2.2", x3.getString("key2"));
assertEquals("USER2", x3.getString("key2[@override]"));
final StringWriter w = new StringWriter();
new FileHandler(x3).save(w);
final String s = w.toString();
x3 = new XMLConfiguration();
new FileHandler(x3).load(new StringReader(s));
assertEquals("value2.2", x3.getString("key2"));
assertEquals("USER2", x3.getString("key2[@override]"));
}
/**
* 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
void testConcurrentAccess() throws ConfigurationException, InterruptedException {
// populate the test combined configuration
setUpSourceTest();
final XMLConfiguration xmlConf = new XMLConfiguration();
new FileHandler(xmlConf).load(ConfigurationAssert.getTestFile("test.xml"));
config.addConfiguration(xmlConf);
final 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
final Synchronizer sync = new ReadWriteSynchronizer();
config.setSynchronizer(sync);
for (final Configuration c : config.getConfigurations()) {
c.setSynchronizer(sync);
}
// setup test threads
final int numberOfReaders = 3;
final int readCount = 5000;
final int writeCount = 3000;
final CountDownLatch latch = new CountDownLatch(1);
final AtomicInteger errorCount = new AtomicInteger();
final Collection<Thread> threads = new ArrayList<>(numberOfReaders + 1);
final Thread writeThread = new WriteThread(config, latch, errorCount, writeCount);
writeThread.start();
threads.add(writeThread);
for (int i = 0; i < numberOfReaders; i++) {
final Thread readThread = new ReadThread(config, latch, errorCount, readCount);
readThread.start();
threads.add(readThread);
}
// perform test
latch.countDown();
for (final Thread t : threads) {
t.join();
}
assertEquals(0, errorCount.get());
}
/**
* Tests whether sub configurations can be created from a key.
*/
@Test
void testConfigurationsAt() {
checkConfigurationsAt(false);
}
/**
* Tests whether sub configurations can be created which are attached.
*/
@Test
void testConfigurationsAtWithUpdates() {
checkConfigurationsAt(true);
}
/**
* Tests using a conversion expression engine for child configurations with strange keys. This test is related to
* CONFIGURATION-336.
*/
@Test
void testConversionExpressionEngine() {
final PropertiesConfiguration child = new PropertiesConfiguration();
child.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
child.addProperty("test(a)", "1,2,3");
config.addConfiguration(child);
final DefaultExpressionEngine engineQuery = new DefaultExpressionEngine(
new DefaultExpressionEngineSymbols.Builder(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS).setIndexStart("<").setIndexEnd(">").create());
config.setExpressionEngine(engineQuery);
final DefaultExpressionEngine engineConvert = new DefaultExpressionEngine(
new DefaultExpressionEngineSymbols.Builder(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS).setIndexStart("[").setIndexEnd("]").create());
config.setConversionExpressionEngine(engineConvert);
assertEquals("1", config.getString("test(a)<0>"));
assertEquals("2", config.getString("test(a)<1>"));
assertEquals("3", config.getString("test(a)<2>"));
}
/**
* Tests whether escaped list delimiters are treated correctly.
*/
@Test
void testEscapeListDelimiters() {
final PropertiesConfiguration sub = new PropertiesConfiguration();
sub.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
sub.addProperty("test.pi", "3\\,1415");
config.addConfiguration(sub);
assertEquals("3,1415", config.getString("test.pi"));
}
/**
* Tests whether access to a configuration by index is correctly synchronized.
*/
@Test
void testGetConfigurationByIdxSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull(config.getConfiguration(0));
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether access to a configuration by name is correctly synchronized.
*/
@Test
void testGetConfigurationByNameSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull(config.getConfiguration(CHILD1));
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
@Test
void testGetConfigurationNameList() throws Exception {
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
final AbstractConfiguration pc = new PropertiesConfiguration();
config.addConfiguration(pc, "props");
final List<String> list = config.getConfigurationNameList();
assertNotNull(list);
assertEquals(3, list.size());
final String name = list.get(1);
assertNotNull(name);
assertEquals(TEST_NAME, name);
}
/**
* Tests whether querying the name list of child configurations is synchronized.
*/
@Test
void testGetConfigurationNameListSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertFalse(config.getConfigurationNameList().isEmpty());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether querying the name set of child configurations is synchronized.
*/
@Test
void testGetConfigurationNamesSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertFalse(config.getConfigurationNames().isEmpty());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
@Test
void testGetConfigurations() throws Exception {
config.addConfiguration(setUpTestConfiguration());
config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
final AbstractConfiguration pc = new PropertiesConfiguration();
config.addConfiguration(pc, "props");
final List<Configuration> list = config.getConfigurations();
assertNotNull(list);
assertEquals(3, list.size());
final Configuration c = list.get(2);
assertSame(pc, c);
}
/**
* Tests whether querying the list of child configurations is synchronized.
*/
@Test
void testGetConfigurationsSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertFalse(config.getConfigurations().isEmpty());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether read access to the conversion expression engine is synchronized.
*/
@Test
void testGetConversionExpressionEngineSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNull(config.getConversionExpressionEngine());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests CONFIGURATION-799.
*/
@Test
void testGetKeys() {
// Set up
final BaseConfiguration conf1 = new BaseConfiguration();
final String key = "x1";
conf1.addProperty(key, 1);
assertEquals(1, conf1.getProperty(key));
final CombinedConfiguration conf2 = new CombinedConfiguration();
conf2.addConfiguration(conf1, null, "");
assertEquals(conf1, conf2.getConfiguration(0));
// Actual test
final Iterator<String> keys = conf2.getKeys();
assertEquals(key, keys.next());
assertFalse(keys.hasNext());
}
/**
* Tests whether getNodeCombiner() is correctly synchronized.
*/
@Test
void testGetNodeCombinerSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull(config.getNodeCombiner());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests whether querying the number of child configurations is synchronized.
*/
@Test
void testGetNumberOfConfigurationsSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertEquals(2, config.getNumberOfConfigurations());
sync.verify(Methods.BEGIN_READ, Methods.END_READ);
checkCombinedRootNotConstructed();
}
/**
* Tests the getSource() method when the passed in key belongs to the combined configuration itself.
*/
@Test
void testGetSourceCombined() {
setUpSourceTest();
final String key = "yet.another.key";
config.addProperty(key, Boolean.TRUE);
assertEquals(config, config.getSource(key));
}
/**
* Tests the gestSource() method when the source property is defined in a hierarchical configuration.
*/
@Test
void testGetSourceHierarchical() {
setUpSourceTest();
assertEquals(config.getConfiguration(CHILD1), config.getSource(TEST_KEY));
}
/**
* Tests the getSource() method when the passed in key refers to multiple values, which are all defined in the same
* source configuration.
*/
@Test
void testGetSourceMulti() {
setUpSourceTest();
final String key = "list.key";
config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
assertEquals(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
void testGetSourceMultiSources() {
setUpSourceTest();
final String key = "list.key";
config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
config.getConfiguration(CHILD2).addProperty(key, "a,b,c");
assertThrows(IllegalArgumentException.class, () -> config.getSource(key));
}
/**
* Tests whether the source configuration can be detected for non hierarchical configurations.
*/
@Test
void testGetSourceNonHierarchical() {
setUpSourceTest();
assertEquals(config.getConfiguration(CHILD2), config.getSource("another.key"));
}
/**
* Tests the getSource() method when a null key is passed in. This should cause an exception.
*/
@Test
void testGetSourceNull() {
assertThrows(IllegalArgumentException.class, () -> config.getSource(null));
}
/**
* Tests whether multiple sources of a key can be retrieved.
*/
@Test
void testGetSourcesMultiSources() {
setUpSourceTest();
final String key = "list.key";
config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
config.getConfiguration(CHILD2).addProperty(key, "a,b,c");
final Set<Configuration> sources = config.getSources(key);
assertEquals(2, sources.size());
assertTrue(sources.contains(config.getConfiguration(CHILD1)));
assertTrue(sources.contains(config.getConfiguration(CHILD2)));
}
/**
* Tests getSources() for a non existing key.
*/
@Test
void testGetSourcesUnknownKey() {
setUpSourceTest();
assertTrue(config.getSources("non.existing,key").isEmpty());
}
/**
* Tests whether getSource() is correctly synchronized.
*/
@Test
void testGetSourceSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
assertNotNull(config.getSource(TEST_KEY));
sync.verifyStart(Methods.BEGIN_READ);
sync.verifyEnd(Methods.END_READ);
}
/**
* Tests the getSource() method when the passed in key is not contained. Result should be null in this case.
*/
@Test
void testGetSourceUnknown() {
setUpSourceTest();
assertNull(config.getSource("an.unknown.key"));
}
/**
* Tests getSource() if a child configuration is again a combined configuration.
*/
@Test
void testGetSourceWithCombinedChildConfiguration() {
setUpSourceTest();
final CombinedConfiguration cc = new CombinedConfiguration();
cc.addConfiguration(config);
assertEquals(config, cc.getSource(TEST_KEY));
}
/**
* Tests accessing a newly created combined configuration.
*/
@Test
void testInit() {
assertEquals(0, config.getNumberOfConfigurations());
assertTrue(config.getConfigurationNames().isEmpty());
assertInstanceOf(UnionCombiner.class, config.getNodeCombiner());
assertNull(config.getConfiguration(TEST_NAME));
}
/**
* Tests whether only a single invalidate event is fired for a change. This test is related to CONFIGURATION-315.
*/
@Test
void testInvalidateEventBeforeAndAfterChange() {
ConfigurationEvent event = new ConfigurationEvent(config, ConfigurationEvent.ANY, null, null, true);
config.onEvent(event);
assertEquals(1, listener.invalidateEvents);
event = new ConfigurationEvent(config, ConfigurationEvent.ANY, null, null, false);
config.onEvent(event);
assertEquals(1, listener.invalidateEvents);
}
/**
* Tests whether invalidate() performs correct synchronization.
*/
@Test
void testInvalidateSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
config.invalidate();
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
}
/**
* Tests whether requested locks are freed correctly if an exception occurs while constructing the root node.
*/
@Test
void testLockHandlingWithExceptionWhenConstructingRootNode() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
final RuntimeException testEx = new ConfigurationRuntimeException("Test exception");
final BaseHierarchicalConfiguration childEx = new BaseHierarchicalConfiguration() {
@Override
public NodeModel<ImmutableNode> getModel() {
throw testEx;
}
};
config.addConfiguration(childEx);
final Exception ex = assertThrows(Exception.class, () -> config.lock(LockMode.READ));
assertEquals(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 removing a configuration.
*/
@Test
void testRemoveConfiguration() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
checkAddConfig(c);
assertTrue(config.removeConfiguration(c));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration by index.
*/
@Test
void testRemoveConfigurationAt() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
assertSame(c, config.removeConfigurationAt(0));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration by name.
*/
@Test
void testRemoveConfigurationByName() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
assertSame(c, config.removeConfiguration(TEST_NAME));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration by name, which is not contained.
*/
@Test
void testRemoveConfigurationByUnknownName() {
assertNull(config.removeConfiguration("unknownName"));
listener.checkEvent(0, 0);
}
/**
* Tests removing a configuration with a name.
*/
@Test
void testRemoveNamedConfiguration() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
config.removeConfiguration(c);
checkRemoveConfig(c);
}
/**
* Tests removing a named configuration by index.
*/
@Test
void testRemoveNamedConfigurationAt() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c, TEST_NAME);
assertSame(c, config.removeConfigurationAt(0));
checkRemoveConfig(c);
}
/**
* Tests removing a configuration that was not added prior.
*/
@Test
void testRemoveNonContainedConfiguration() {
assertFalse(config.removeConfiguration(setUpTestConfiguration()));
listener.checkEvent(0, 0);
}
/**
* Tests whether write access to the conversion expression engine is synchronized.
*/
@Test
void testSetConversionExpressionEngineSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
config.setConversionExpressionEngine(new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS));
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
checkCombinedRootNotConstructed();
}
/**
* Tests if setting a node combiner causes an invalidation.
*/
@Test
void testSetNodeCombiner() {
final NodeCombiner combiner = new UnionCombiner();
config.setNodeCombiner(combiner);
assertSame(combiner, config.getNodeCombiner());
listener.checkEvent(1, 0);
}
/**
* Tests whether setNodeCombiner() is correctly synchronized.
*/
@Test
void testSetNodeCombinerSynchronized() {
final SynchronizerTestImpl sync = setUpSynchronizerTest();
config.setNodeCombiner(new UnionCombiner());
sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
checkCombinedRootNotConstructed();
}
/**
* Tests setting a null node combiner. This should cause an exception.
*/
@Test
void testSetNullNodeCombiner() {
assertThrows(IllegalArgumentException.class, () -> config.setNodeCombiner(null));
}
/**
* Tests whether a sub configuration survives updates of its parent.
*/
@Test
void testSubConfigurationWithUpdates() {
final AbstractConfiguration srcConfig = setUpSubConfigTest();
final HierarchicalConfiguration<ImmutableNode> sub = config.configurationAt(SUB_KEY, true);
assertTrue(sub.getBoolean(TEST_KEY));
srcConfig.setProperty(TEST_KEY, Boolean.FALSE);
assertFalse(sub.getBoolean(TEST_KEY));
assertFalse(config.getBoolean(SUB_KEY + '.' + TEST_KEY));
}
/**
* Tests if an update of a contained configuration leeds to an invalidation of the combined configuration.
*/
@Test
void testUpdateContainedConfiguration() {
final AbstractConfiguration c = setUpTestConfiguration();
config.addConfiguration(c);
c.addProperty("test.otherTest", "yes");
assertEquals("yes", config.getString("test.otherTest"));
listener.checkEvent(2, 0);
}
}