View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
22  import static org.junit.jupiter.api.Assertions.assertNotEquals;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNotSame;
25  import static org.junit.jupiter.api.Assertions.assertNull;
26  import static org.junit.jupiter.api.Assertions.assertSame;
27  import static org.junit.jupiter.api.Assertions.assertThrows;
28  import static org.junit.jupiter.api.Assertions.assertTrue;
29  
30  import java.io.StringReader;
31  import java.io.StringWriter;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.Collections;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Set;
38  import java.util.concurrent.CountDownLatch;
39  import java.util.concurrent.atomic.AtomicInteger;
40  
41  import org.apache.commons.configuration2.SynchronizerTestImpl.Methods;
42  import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
43  import org.apache.commons.configuration2.event.ConfigurationEvent;
44  import org.apache.commons.configuration2.event.EventListener;
45  import org.apache.commons.configuration2.ex.ConfigurationException;
46  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
47  import org.apache.commons.configuration2.io.FileHandler;
48  import org.apache.commons.configuration2.sync.LockMode;
49  import org.apache.commons.configuration2.sync.ReadWriteSynchronizer;
50  import org.apache.commons.configuration2.sync.Synchronizer;
51  import org.apache.commons.configuration2.tree.DefaultExpressionEngine;
52  import org.apache.commons.configuration2.tree.DefaultExpressionEngineSymbols;
53  import org.apache.commons.configuration2.tree.ImmutableNode;
54  import org.apache.commons.configuration2.tree.NodeCombiner;
55  import org.apache.commons.configuration2.tree.NodeModel;
56  import org.apache.commons.configuration2.tree.OverrideCombiner;
57  import org.apache.commons.configuration2.tree.UnionCombiner;
58  import org.junit.jupiter.api.BeforeEach;
59  import org.junit.jupiter.api.Test;
60  
61  /**
62   * Test class for CombinedConfiguration.
63   */
64  public class TestCombinedConfiguration {
65  
66      /**
67       * Test event listener class for checking if the expected invalidate events are fired.
68       */
69      private static final class CombinedListener implements EventListener<ConfigurationEvent> {
70  
71          private int invalidateEvents;
72  
73          private int otherEvents;
74  
75          /**
76           * Checks if the expected number of events was fired.
77           *
78           * @param expectedInvalidate the expected number of invalidate events
79           * @param expectedOthers the expected number of other events
80           */
81          public void checkEvent(final int expectedInvalidate, final int expectedOthers) {
82              assertEquals(expectedInvalidate, invalidateEvents);
83              assertEquals(expectedOthers, otherEvents);
84          }
85  
86          @Override
87          public void onEvent(final ConfigurationEvent event) {
88              if (event.getEventType() == CombinedConfiguration.COMBINED_INVALIDATE) {
89                  invalidateEvents++;
90              } else {
91                  otherEvents++;
92              }
93          }
94      }
95  
96      /**
97       * A test thread performing reads on a combined configuration. This thread reads a certain property from the
98       * configuration. If everything works well, this property should have at least one and at most two values.
99       */
100     private static final class ReadThread extends Thread {
101 
102         /** The configuration to be accessed. */
103         private final Configuration config;
104 
105         /** The latch for synchronizing thread start. */
106         private final CountDownLatch startLatch;
107 
108         /** A counter for read errors. */
109         private final AtomicInteger errorCount;
110 
111         /** The number of reads to be performed. */
112         private final int numberOfReads;
113 
114         /**
115          * Creates a new instance of {@code ReadThread}.
116          *
117          * @param readConfig the configuration to be read
118          * @param latch the latch for synchronizing thread start
119          * @param errCnt the counter for read errors
120          * @param readCount the number of reads to be performed
121          */
122         public ReadThread(final Configuration readConfig, final CountDownLatch latch, final AtomicInteger errCnt, final int readCount) {
123             config = readConfig;
124             startLatch = latch;
125             errorCount = errCnt;
126             numberOfReads = readCount;
127         }
128 
129         /**
130          * Reads the test property from the associated configuration. Its values are checked.
131          */
132         private void readConfiguration() {
133             final List<Object> values = config.getList(KEY_CONCURRENT);
134             if (values.size() < 1 || values.size() > 2) {
135                 errorCount.incrementAndGet();
136             } else {
137                 boolean ok = true;
138                 for (final Object value : values) {
139                     if (!TEST_NAME.equals(value)) {
140                         ok = false;
141                     }
142                 }
143                 if (!ok) {
144                     errorCount.incrementAndGet();
145                 }
146             }
147         }
148 
149         /**
150          * Reads from the test configuration.
151          */
152         @Override
153         public void run() {
154             try {
155                 startLatch.await();
156                 for (int i = 0; i < numberOfReads; i++) {
157                     readConfiguration();
158                 }
159             } catch (final Exception e) {
160                 errorCount.incrementAndGet();
161             }
162         }
163     }
164 
165     /**
166      * A test thread performing updates on a test configuration. This thread modifies configurations which are children of a
167      * combined configuration. Each update operation adds a value to one of the child configurations and removes it from
168      * another one (which contained it before). So if concurrent reads are performed, the test property should always have
169      * between 1 and 2 values.
170      */
171     private static final class WriteThread extends Thread {
172 
173         /** The list with the child configurations. */
174         private final List<Configuration> testConfigs;
175 
176         /** The latch for synchronizing thread start. */
177         private final CountDownLatch startLatch;
178 
179         /** A counter for errors. */
180         private final AtomicInteger errorCount;
181 
182         /** The number of write operations to be performed. */
183         private final int numberOfWrites;
184 
185         /** The index of the child configuration containing the test property. */
186         private int currentChildConfigIdx;
187 
188         /**
189          * Creates a new instance of {@code WriteThread}.
190          *
191          * @param cc the test combined configuration
192          * @param latch the latch for synchronizing test start
193          * @param errCnt a counter for errors
194          * @param writeCount the number of writes to be performed
195          */
196         public WriteThread(final CombinedConfiguration cc, final CountDownLatch latch, final AtomicInteger errCnt, final int writeCount) {
197             testConfigs = cc.getConfigurations();
198             startLatch = latch;
199             errorCount = errCnt;
200             numberOfWrites = writeCount;
201         }
202 
203         @Override
204         public void run() {
205             try {
206                 startLatch.await();
207                 for (int i = 0; i < numberOfWrites; i++) {
208                     updateConfigurations();
209                 }
210             } catch (final InterruptedException e) {
211                 errorCount.incrementAndGet();
212             }
213         }
214 
215         /**
216          * Performs the update operation.
217          */
218         private void updateConfigurations() {
219             final int newIdx = (currentChildConfigIdx + 1) % testConfigs.size();
220             testConfigs.get(newIdx).addProperty(KEY_CONCURRENT, TEST_NAME);
221             testConfigs.get(currentChildConfigIdx).clearProperty(KEY_CONCURRENT);
222             currentChildConfigIdx = newIdx;
223         }
224     }
225 
226     /** Constant for the name of a sub configuration. */
227     private static final String TEST_NAME = "SUBCONFIG";
228 
229     /** Constant for a test key. */
230     private static final String TEST_KEY = "test.value";
231 
232     /** Constant for a key to be used for a concurrent test. */
233     private static final String KEY_CONCURRENT = "concurrent.access.test";
234 
235     /** Constant for the name of the first child configuration. */
236     private static final String CHILD1 = TEST_NAME + "1";
237 
238     /** Constant for the name of the second child configuration. */
239     private static final String CHILD2 = TEST_NAME + "2";
240 
241     /** Constant for the key for a sub configuration. */
242     private static final String SUB_KEY = "test.sub.config";
243 
244     /**
245      * Helper method for creating a test configuration to be added to the combined configuration.
246      *
247      * @return the test configuration
248      */
249     private static AbstractConfiguration setUpTestConfiguration() {
250         final BaseHierarchicalConfiguration config = new BaseHierarchicalConfiguration();
251         config.addProperty(TEST_KEY, Boolean.TRUE);
252         config.addProperty("test.comment", "This is a test");
253         return config;
254     }
255 
256     /** The configuration to be tested. */
257     private CombinedConfiguration config;
258 
259     /** The test event listener. */
260     private CombinedListener listener;
261 
262     /**
263      * Checks if a configuration was correctly added to the combined config.
264      *
265      * @param c the config to check
266      */
267     private void checkAddConfig(final AbstractConfiguration c) {
268         final Collection<EventListener<? super ConfigurationEvent>> listeners = c.getEventListeners(ConfigurationEvent.ANY);
269         assertEquals(1, listeners.size());
270         assertTrue(listeners.contains(config));
271     }
272 
273     /**
274      * Helper method for testing that the combined root node has not yet been constructed.
275      */
276     private void checkCombinedRootNotConstructed() {
277         assertTrue(config.getModel().getNodeHandler().getRootNode().getChildren().isEmpty());
278     }
279 
280     /**
281      * Checks the configurationsAt() method.
282      *
283      * @param withUpdates flag whether updates are supported
284      */
285     private void checkConfigurationsAt(final boolean withUpdates) {
286         setUpSubConfigTest();
287         final List<HierarchicalConfiguration<ImmutableNode>> subs = config.configurationsAt(SUB_KEY, withUpdates);
288         assertEquals(1, subs.size());
289         assertTrue(subs.get(0).getBoolean(TEST_KEY));
290     }
291 
292     /**
293      * Tests whether a configuration was completely removed.
294      *
295      * @param c the removed configuration
296      */
297     private void checkRemoveConfig(final AbstractConfiguration c) {
298         assertTrue(c.getEventListeners(ConfigurationEvent.ANY).isEmpty());
299         assertEquals(0, config.getNumberOfConfigurations());
300         assertTrue(config.getConfigurationNames().isEmpty());
301         listener.checkEvent(2, 0);
302     }
303 
304     @BeforeEach
305     public void setUp() throws Exception {
306         config = new CombinedConfiguration();
307         listener = new CombinedListener();
308         config.addEventListener(ConfigurationEvent.ANY, listener);
309     }
310 
311     /**
312      * Prepares a test of the getSource() method.
313      */
314     private void setUpSourceTest() {
315         final BaseHierarchicalConfiguration c1 = new BaseHierarchicalConfiguration();
316         final PropertiesConfiguration c2 = new PropertiesConfiguration();
317         c1.addProperty(TEST_KEY, TEST_NAME);
318         c2.addProperty("another.key", "test");
319         config.addConfiguration(c1, CHILD1);
320         config.addConfiguration(c2, CHILD2);
321     }
322 
323     /**
324      * Prepares the test configuration for a test for sub configurations. Some child configurations are added.
325      *
326      * @return the sub configuration at the test sub key
327      */
328     private AbstractConfiguration setUpSubConfigTest() {
329         final AbstractConfiguration srcConfig = setUpTestConfiguration();
330         config.addConfiguration(srcConfig, "source", SUB_KEY);
331         config.addConfiguration(setUpTestConfiguration());
332         config.addConfiguration(setUpTestConfiguration(), "otherTest", "other.prefix");
333         return srcConfig;
334     }
335 
336     /**
337      * Prepares a test for synchronization. This method installs a test synchronizer and adds some test configurations.
338      *
339      * @return the test synchronizer
340      */
341     private SynchronizerTestImpl setUpSynchronizerTest() {
342         setUpSourceTest();
343         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
344         config.setSynchronizer(sync);
345         return sync;
346     }
347 
348     /**
349      * Tests accessing properties if no configurations have been added.
350      */
351     @Test
352     void testAccessPropertyEmpty() {
353         assertFalse(config.containsKey(TEST_KEY));
354         assertNull(config.getString("test.comment"));
355         assertTrue(config.isEmpty());
356     }
357 
358     /**
359      * Tests accessing properties if multiple configurations have been added.
360      */
361     @Test
362     void testAccessPropertyMulti() {
363         config.addConfiguration(setUpTestConfiguration());
364         config.addConfiguration(setUpTestConfiguration(), null, "prefix1");
365         config.addConfiguration(setUpTestConfiguration(), null, "prefix2");
366         assertTrue(config.getBoolean(TEST_KEY));
367         assertTrue(config.getBoolean("prefix1." + TEST_KEY));
368         assertTrue(config.getBoolean("prefix2." + TEST_KEY));
369         assertFalse(config.isEmpty());
370         listener.checkEvent(3, 0);
371     }
372 
373     /**
374      * Tests adding a configuration (without further information).
375      */
376     @Test
377     void testAddConfiguration() {
378         final AbstractConfiguration c = setUpTestConfiguration();
379         config.addConfiguration(c);
380         checkAddConfig(c);
381         assertEquals(1, config.getNumberOfConfigurations());
382         assertTrue(config.getConfigurationNames().isEmpty());
383         assertSame(c, config.getConfiguration(0));
384         assertTrue(config.getBoolean(TEST_KEY));
385         listener.checkEvent(1, 0);
386     }
387 
388     /**
389      * Tests adding a configuration and specifying an at position.
390      */
391     @Test
392     void testAddConfigurationAt() {
393         final AbstractConfiguration c = setUpTestConfiguration();
394         config.addConfiguration(c, null, "my");
395         checkAddConfig(c);
396         assertTrue(config.getBoolean("my." + TEST_KEY));
397     }
398 
399     /**
400      * Tests adding a configuration with a complex at position. Here the at path contains a dot, which must be escaped.
401      */
402     @Test
403     void testAddConfigurationComplexAt() {
404         final AbstractConfiguration c = setUpTestConfiguration();
405         config.addConfiguration(c, null, "This..is.a.complex");
406         checkAddConfig(c);
407         assertTrue(config.getBoolean("This..is.a.complex." + TEST_KEY));
408     }
409 
410     /**
411      * Tests whether adding a new configuration is synchronized.
412      */
413     @Test
414     void testAddConfigurationSynchronized() {
415         final SynchronizerTestImpl sync = setUpSynchronizerTest();
416         config.addConfiguration(new BaseHierarchicalConfiguration());
417         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
418         checkCombinedRootNotConstructed();
419     }
420 
421     /**
422      * Tests adding a configuration with a name.
423      */
424     @Test
425     void testAddConfigurationWithName() {
426         final AbstractConfiguration c = setUpTestConfiguration();
427         config.addConfiguration(c, TEST_NAME);
428         checkAddConfig(c);
429         assertEquals(1, config.getNumberOfConfigurations());
430         assertSame(c, config.getConfiguration(0));
431         assertSame(c, config.getConfiguration(TEST_NAME));
432         final Set<String> names = config.getConfigurationNames();
433         assertEquals(1, names.size());
434         assertTrue(names.contains(TEST_NAME));
435         assertTrue(config.getBoolean(TEST_KEY));
436         listener.checkEvent(1, 0);
437     }
438 
439     /**
440      * Tests adding a configuration with a name when this name already exists. This should cause an exception.
441      */
442     @Test
443     void testAddConfigurationWithNameTwice() {
444         config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
445         final Configuration configuration = setUpTestConfiguration();
446         assertThrows(ConfigurationRuntimeException.class, () -> config.addConfiguration(configuration, TEST_NAME, "prefix"));
447     }
448 
449     /**
450      * Tests adding a null configuration. This should cause an exception to be thrown.
451      */
452     @Test
453     void testAddNullConfiguration() {
454         assertThrows(IllegalArgumentException.class, () -> config.addConfiguration(null));
455     }
456 
457     /**
458      * Tests clearing a combined configuration. This should remove all contained configurations.
459      */
460     @Test
461     void testClear() {
462         config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "test");
463         config.addConfiguration(setUpTestConfiguration());
464 
465         config.clear();
466         assertEquals(0, config.getNumberOfConfigurations());
467         assertTrue(config.getConfigurationNames().isEmpty());
468         assertTrue(config.isEmpty());
469 
470         listener.checkEvent(3, 2);
471     }
472 
473     /**
474      * Tests whether the combined configuration removes itself as change listener from the child configurations on a clear
475      * operation. This test is related to CONFIGURATION-572.
476      */
477     @Test
478     void testClearRemoveChildListener() {
479         final AbstractConfiguration child = setUpTestConfiguration();
480         config.addConfiguration(child);
481 
482         config.clear();
483         for (final EventListener<?> listener : child.getEventListeners(ConfigurationEvent.ANY)) {
484             assertNotEquals(config, listener);
485         }
486     }
487 
488     /**
489      * Tests cloning a combined configuration.
490      */
491     @Test
492     void testClone() {
493         config.addConfiguration(setUpTestConfiguration());
494         config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
495         config.addConfiguration(new PropertiesConfiguration(), "props");
496 
497         final CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
498         assertNotNull(cc2.getModel().getNodeHandler().getRootNode());
499         assertEquals(config.getNumberOfConfigurations(), cc2.getNumberOfConfigurations());
500         assertSame(config.getNodeCombiner(), cc2.getNodeCombiner());
501         assertEquals(config.getConfigurationNames().size(), cc2.getConfigurationNames().size());
502         assertTrue(Collections.disjoint(cc2.getEventListeners(ConfigurationEvent.ANY), config.getEventListeners(ConfigurationEvent.ANY)));
503 
504         final StrictConfigurationComparator comp = new StrictConfigurationComparator();
505         for (int i = 0; i < config.getNumberOfConfigurations(); i++) {
506             assertNotSame(config.getConfiguration(i), cc2.getConfiguration(i), "Configuration at " + i + " was not cloned");
507             assertEquals(config.getConfiguration(i).getClass(), cc2.getConfiguration(i).getClass(), "Wrong config class at " + i);
508             assertTrue(comp.compare(config.getConfiguration(i), cc2.getConfiguration(i)), "Configs not equal at " + i);
509         }
510 
511         assertTrue(comp.compare(config, cc2));
512     }
513 
514     /**
515      * Tests if the cloned configuration is decoupled from the original.
516      */
517     @Test
518     void testCloneModify() {
519         config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
520         final CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
521         assertTrue(cc2.getConfigurationNames().contains(TEST_NAME));
522         cc2.removeConfiguration(TEST_NAME);
523         assertFalse(config.getConfigurationNames().isEmpty());
524     }
525 
526     /**
527      * Tests whether cloning of a configuration is correctly synchronized.
528      */
529     @Test
530     void testCloneSynchronized() {
531         setUpSourceTest();
532         config.lock(LockMode.READ); // Causes the root node to be constructed
533         config.unlock(LockMode.READ);
534         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
535         config.setSynchronizer(sync);
536         config.clone();
537         // clone() of base class is wrapped by another read lock
538         sync.verifyStart(Methods.BEGIN_READ, Methods.BEGIN_READ);
539         sync.verifyEnd(Methods.END_READ, Methods.END_READ);
540     }
541 
542     /**
543      * Tests whether a combined configuration can be copied to an XML configuration. This test is related to
544      * CONFIGURATION-445.
545      */
546     @Test
547     void testCombinedCopyToXML() throws ConfigurationException {
548         final XMLConfiguration x1 = new XMLConfiguration();
549         x1.addProperty("key1", "value1");
550         x1.addProperty("key1[@override]", "USER1");
551         x1.addProperty("key2", "value2");
552         x1.addProperty("key2[@override]", "USER2");
553         final XMLConfiguration x2 = new XMLConfiguration();
554         x2.addProperty("key2", "value2.2");
555         x2.addProperty("key2[@override]", "USER2");
556         config.setNodeCombiner(new OverrideCombiner());
557         config.addConfiguration(x2);
558         config.addConfiguration(x1);
559         XMLConfiguration x3 = new XMLConfiguration(config);
560         assertEquals("value2.2", x3.getString("key2"));
561         assertEquals("USER2", x3.getString("key2[@override]"));
562         final StringWriter w = new StringWriter();
563         new FileHandler(x3).save(w);
564         final String s = w.toString();
565         x3 = new XMLConfiguration();
566         new FileHandler(x3).load(new StringReader(s));
567         assertEquals("value2.2", x3.getString("key2"));
568         assertEquals("USER2", x3.getString("key2[@override]"));
569     }
570 
571     /**
572      * Tests concurrent read and write access on a combined configuration. There are multiple reader threads and a single
573      * writer thread. It is checked that no inconsistencies occur.
574      */
575     @Test
576     void testConcurrentAccess() throws ConfigurationException, InterruptedException {
577         // populate the test combined configuration
578         setUpSourceTest();
579         final XMLConfiguration xmlConf = new XMLConfiguration();
580         new FileHandler(xmlConf).load(ConfigurationAssert.getTestFile("test.xml"));
581         config.addConfiguration(xmlConf);
582         final PropertiesConfiguration propConf = new PropertiesConfiguration();
583         new FileHandler(propConf).load(ConfigurationAssert.getTestFile("test.properties"));
584         for (int i = 0; i < 8; i++) {
585             config.addConfiguration(new BaseHierarchicalConfiguration());
586         }
587         config.getConfiguration(0).addProperty(KEY_CONCURRENT, TEST_NAME);
588 
589         // Set a single synchronizer for all involved configurations
590         final Synchronizer sync = new ReadWriteSynchronizer();
591         config.setSynchronizer(sync);
592         for (final Configuration c : config.getConfigurations()) {
593             c.setSynchronizer(sync);
594         }
595 
596         // setup test threads
597         final int numberOfReaders = 3;
598         final int readCount = 5000;
599         final int writeCount = 3000;
600         final CountDownLatch latch = new CountDownLatch(1);
601         final AtomicInteger errorCount = new AtomicInteger();
602         final Collection<Thread> threads = new ArrayList<>(numberOfReaders + 1);
603         final Thread writeThread = new WriteThread(config, latch, errorCount, writeCount);
604         writeThread.start();
605         threads.add(writeThread);
606         for (int i = 0; i < numberOfReaders; i++) {
607             final Thread readThread = new ReadThread(config, latch, errorCount, readCount);
608             readThread.start();
609             threads.add(readThread);
610         }
611 
612         // perform test
613         latch.countDown();
614         for (final Thread t : threads) {
615             t.join();
616         }
617         assertEquals(0, errorCount.get());
618     }
619 
620     /**
621      * Tests whether sub configurations can be created from a key.
622      */
623     @Test
624     void testConfigurationsAt() {
625         checkConfigurationsAt(false);
626     }
627 
628     /**
629      * Tests whether sub configurations can be created which are attached.
630      */
631     @Test
632     void testConfigurationsAtWithUpdates() {
633         checkConfigurationsAt(true);
634     }
635 
636     /**
637      * Tests using a conversion expression engine for child configurations with strange keys. This test is related to
638      * CONFIGURATION-336.
639      */
640     @Test
641     void testConversionExpressionEngine() {
642         final PropertiesConfiguration child = new PropertiesConfiguration();
643         child.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
644         child.addProperty("test(a)", "1,2,3");
645         config.addConfiguration(child);
646         final DefaultExpressionEngine engineQuery = new DefaultExpressionEngine(
647             new DefaultExpressionEngineSymbols.Builder(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS).setIndexStart("<").setIndexEnd(">").create());
648         config.setExpressionEngine(engineQuery);
649         final DefaultExpressionEngine engineConvert = new DefaultExpressionEngine(
650             new DefaultExpressionEngineSymbols.Builder(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS).setIndexStart("[").setIndexEnd("]").create());
651         config.setConversionExpressionEngine(engineConvert);
652         assertEquals("1", config.getString("test(a)<0>"));
653         assertEquals("2", config.getString("test(a)<1>"));
654         assertEquals("3", config.getString("test(a)<2>"));
655     }
656 
657     /**
658      * Tests whether escaped list delimiters are treated correctly.
659      */
660     @Test
661     void testEscapeListDelimiters() {
662         final PropertiesConfiguration sub = new PropertiesConfiguration();
663         sub.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
664         sub.addProperty("test.pi", "3\\,1415");
665         config.addConfiguration(sub);
666         assertEquals("3,1415", config.getString("test.pi"));
667     }
668 
669     /**
670      * Tests whether access to a configuration by index is correctly synchronized.
671      */
672     @Test
673     void testGetConfigurationByIdxSynchronized() {
674         final SynchronizerTestImpl sync = setUpSynchronizerTest();
675         assertNotNull(config.getConfiguration(0));
676         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
677         checkCombinedRootNotConstructed();
678     }
679 
680     /**
681      * Tests whether access to a configuration by name is correctly synchronized.
682      */
683     @Test
684     void testGetConfigurationByNameSynchronized() {
685         final SynchronizerTestImpl sync = setUpSynchronizerTest();
686         assertNotNull(config.getConfiguration(CHILD1));
687         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
688         checkCombinedRootNotConstructed();
689     }
690 
691     @Test
692     void testGetConfigurationNameList() throws Exception {
693         config.addConfiguration(setUpTestConfiguration());
694         config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
695         final AbstractConfiguration pc = new PropertiesConfiguration();
696         config.addConfiguration(pc, "props");
697         final List<String> list = config.getConfigurationNameList();
698         assertNotNull(list);
699         assertEquals(3, list.size());
700         final String name = list.get(1);
701         assertNotNull(name);
702         assertEquals(TEST_NAME, name);
703     }
704 
705     /**
706      * Tests whether querying the name list of child configurations is synchronized.
707      */
708     @Test
709     void testGetConfigurationNameListSynchronized() {
710         final SynchronizerTestImpl sync = setUpSynchronizerTest();
711         assertFalse(config.getConfigurationNameList().isEmpty());
712         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
713         checkCombinedRootNotConstructed();
714     }
715 
716     /**
717      * Tests whether querying the name set of child configurations is synchronized.
718      */
719     @Test
720     void testGetConfigurationNamesSynchronized() {
721         final SynchronizerTestImpl sync = setUpSynchronizerTest();
722         assertFalse(config.getConfigurationNames().isEmpty());
723         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
724         checkCombinedRootNotConstructed();
725     }
726 
727     @Test
728     void testGetConfigurations() throws Exception {
729         config.addConfiguration(setUpTestConfiguration());
730         config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
731         final AbstractConfiguration pc = new PropertiesConfiguration();
732         config.addConfiguration(pc, "props");
733         final List<Configuration> list = config.getConfigurations();
734         assertNotNull(list);
735         assertEquals(3, list.size());
736         final Configuration c = list.get(2);
737         assertSame(pc, c);
738     }
739 
740     /**
741      * Tests whether querying the list of child configurations is synchronized.
742      */
743     @Test
744     void testGetConfigurationsSynchronized() {
745         final SynchronizerTestImpl sync = setUpSynchronizerTest();
746         assertFalse(config.getConfigurations().isEmpty());
747         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
748         checkCombinedRootNotConstructed();
749     }
750 
751     /**
752      * Tests whether read access to the conversion expression engine is synchronized.
753      */
754     @Test
755     void testGetConversionExpressionEngineSynchronized() {
756         final SynchronizerTestImpl sync = setUpSynchronizerTest();
757         assertNull(config.getConversionExpressionEngine());
758         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
759         checkCombinedRootNotConstructed();
760     }
761 
762     /**
763      * Tests CONFIGURATION-799.
764      */
765     @Test
766     void testGetKeys() {
767         // Set up
768         final BaseConfiguration conf1 = new BaseConfiguration();
769         final String key = "x1";
770         conf1.addProperty(key, 1);
771         assertEquals(1, conf1.getProperty(key));
772 
773         final CombinedConfiguration conf2 = new CombinedConfiguration();
774         conf2.addConfiguration(conf1, null, "");
775         assertEquals(conf1, conf2.getConfiguration(0));
776 
777         // Actual test
778         final Iterator<String> keys = conf2.getKeys();
779         assertEquals(key, keys.next());
780         assertFalse(keys.hasNext());
781     }
782 
783     /**
784      * Tests whether getNodeCombiner() is correctly synchronized.
785      */
786     @Test
787     void testGetNodeCombinerSynchronized() {
788         final SynchronizerTestImpl sync = setUpSynchronizerTest();
789         assertNotNull(config.getNodeCombiner());
790         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
791         checkCombinedRootNotConstructed();
792     }
793 
794     /**
795      * Tests whether querying the number of child configurations is synchronized.
796      */
797     @Test
798     void testGetNumberOfConfigurationsSynchronized() {
799         final SynchronizerTestImpl sync = setUpSynchronizerTest();
800         assertEquals(2, config.getNumberOfConfigurations());
801         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
802         checkCombinedRootNotConstructed();
803     }
804 
805     /**
806      * Tests the getSource() method when the passed in key belongs to the combined configuration itself.
807      */
808     @Test
809     void testGetSourceCombined() {
810         setUpSourceTest();
811         final String key = "yet.another.key";
812         config.addProperty(key, Boolean.TRUE);
813         assertEquals(config, config.getSource(key));
814     }
815 
816     /**
817      * Tests the gestSource() method when the source property is defined in a hierarchical configuration.
818      */
819     @Test
820     void testGetSourceHierarchical() {
821         setUpSourceTest();
822         assertEquals(config.getConfiguration(CHILD1), config.getSource(TEST_KEY));
823     }
824 
825     /**
826      * Tests the getSource() method when the passed in key refers to multiple values, which are all defined in the same
827      * source configuration.
828      */
829     @Test
830     void testGetSourceMulti() {
831         setUpSourceTest();
832         final String key = "list.key";
833         config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
834         assertEquals(config.getConfiguration(CHILD1), config.getSource(key));
835     }
836 
837     /**
838      * Tests the getSource() method when the passed in key refers to multiple values defined by different sources. This
839      * should cause an exception.
840      */
841     @Test
842     void testGetSourceMultiSources() {
843         setUpSourceTest();
844         final String key = "list.key";
845         config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
846         config.getConfiguration(CHILD2).addProperty(key, "a,b,c");
847         assertThrows(IllegalArgumentException.class, () -> config.getSource(key));
848     }
849 
850     /**
851      * Tests whether the source configuration can be detected for non hierarchical configurations.
852      */
853     @Test
854     void testGetSourceNonHierarchical() {
855         setUpSourceTest();
856         assertEquals(config.getConfiguration(CHILD2), config.getSource("another.key"));
857     }
858 
859     /**
860      * Tests the getSource() method when a null key is passed in. This should cause an exception.
861      */
862     @Test
863     void testGetSourceNull() {
864         assertThrows(IllegalArgumentException.class, () -> config.getSource(null));
865     }
866 
867     /**
868      * Tests whether multiple sources of a key can be retrieved.
869      */
870     @Test
871     void testGetSourcesMultiSources() {
872         setUpSourceTest();
873         final String key = "list.key";
874         config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
875         config.getConfiguration(CHILD2).addProperty(key, "a,b,c");
876         final Set<Configuration> sources = config.getSources(key);
877         assertEquals(2, sources.size());
878         assertTrue(sources.contains(config.getConfiguration(CHILD1)));
879         assertTrue(sources.contains(config.getConfiguration(CHILD2)));
880     }
881 
882     /**
883      * Tests getSources() for a non existing key.
884      */
885     @Test
886     void testGetSourcesUnknownKey() {
887         setUpSourceTest();
888         assertTrue(config.getSources("non.existing,key").isEmpty());
889     }
890 
891     /**
892      * Tests whether getSource() is correctly synchronized.
893      */
894     @Test
895     void testGetSourceSynchronized() {
896         final SynchronizerTestImpl sync = setUpSynchronizerTest();
897         assertNotNull(config.getSource(TEST_KEY));
898         sync.verifyStart(Methods.BEGIN_READ);
899         sync.verifyEnd(Methods.END_READ);
900     }
901 
902     /**
903      * Tests the getSource() method when the passed in key is not contained. Result should be null in this case.
904      */
905     @Test
906     void testGetSourceUnknown() {
907         setUpSourceTest();
908         assertNull(config.getSource("an.unknown.key"));
909     }
910 
911     /**
912      * Tests getSource() if a child configuration is again a combined configuration.
913      */
914     @Test
915     void testGetSourceWithCombinedChildConfiguration() {
916         setUpSourceTest();
917         final CombinedConfiguration cc = new CombinedConfiguration();
918         cc.addConfiguration(config);
919         assertEquals(config, cc.getSource(TEST_KEY));
920     }
921 
922     /**
923      * Tests accessing a newly created combined configuration.
924      */
925     @Test
926     void testInit() {
927         assertEquals(0, config.getNumberOfConfigurations());
928         assertTrue(config.getConfigurationNames().isEmpty());
929         assertInstanceOf(UnionCombiner.class, config.getNodeCombiner());
930         assertNull(config.getConfiguration(TEST_NAME));
931     }
932 
933     /**
934      * Tests whether only a single invalidate event is fired for a change. This test is related to CONFIGURATION-315.
935      */
936     @Test
937     void testInvalidateEventBeforeAndAfterChange() {
938         ConfigurationEvent event = new ConfigurationEvent(config, ConfigurationEvent.ANY, null, null, true);
939         config.onEvent(event);
940         assertEquals(1, listener.invalidateEvents);
941         event = new ConfigurationEvent(config, ConfigurationEvent.ANY, null, null, false);
942         config.onEvent(event);
943         assertEquals(1, listener.invalidateEvents);
944     }
945 
946     /**
947      * Tests whether invalidate() performs correct synchronization.
948      */
949     @Test
950     void testInvalidateSynchronized() {
951         final SynchronizerTestImpl sync = setUpSynchronizerTest();
952         config.invalidate();
953         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
954     }
955 
956     /**
957      * Tests whether requested locks are freed correctly if an exception occurs while constructing the root node.
958      */
959     @Test
960     void testLockHandlingWithExceptionWhenConstructingRootNode() {
961         final SynchronizerTestImpl sync = setUpSynchronizerTest();
962         final RuntimeException testEx = new ConfigurationRuntimeException("Test exception");
963         final BaseHierarchicalConfiguration childEx = new BaseHierarchicalConfiguration() {
964             @Override
965             public NodeModel<ImmutableNode> getModel() {
966                 throw testEx;
967             }
968         };
969         config.addConfiguration(childEx);
970         final Exception ex = assertThrows(Exception.class, () -> config.lock(LockMode.READ));
971         assertEquals(testEx, ex);
972         // 1 x add configuration, then obtain read lock and create root node
973         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_READ, Methods.END_READ, Methods.BEGIN_WRITE, Methods.END_WRITE);
974     }
975 
976     /**
977      * Tests removing a configuration.
978      */
979     @Test
980     void testRemoveConfiguration() {
981         final AbstractConfiguration c = setUpTestConfiguration();
982         config.addConfiguration(c);
983         checkAddConfig(c);
984         assertTrue(config.removeConfiguration(c));
985         checkRemoveConfig(c);
986     }
987 
988     /**
989      * Tests removing a configuration by index.
990      */
991     @Test
992     void testRemoveConfigurationAt() {
993         final AbstractConfiguration c = setUpTestConfiguration();
994         config.addConfiguration(c);
995         assertSame(c, config.removeConfigurationAt(0));
996         checkRemoveConfig(c);
997     }
998 
999     /**
1000      * Tests removing a configuration by name.
1001      */
1002     @Test
1003     void testRemoveConfigurationByName() {
1004         final AbstractConfiguration c = setUpTestConfiguration();
1005         config.addConfiguration(c, TEST_NAME);
1006         assertSame(c, config.removeConfiguration(TEST_NAME));
1007         checkRemoveConfig(c);
1008     }
1009 
1010     /**
1011      * Tests removing a configuration by name, which is not contained.
1012      */
1013     @Test
1014     void testRemoveConfigurationByUnknownName() {
1015         assertNull(config.removeConfiguration("unknownName"));
1016         listener.checkEvent(0, 0);
1017     }
1018 
1019     /**
1020      * Tests removing a configuration with a name.
1021      */
1022     @Test
1023     void testRemoveNamedConfiguration() {
1024         final AbstractConfiguration c = setUpTestConfiguration();
1025         config.addConfiguration(c, TEST_NAME);
1026         config.removeConfiguration(c);
1027         checkRemoveConfig(c);
1028     }
1029 
1030     /**
1031      * Tests removing a named configuration by index.
1032      */
1033     @Test
1034     void testRemoveNamedConfigurationAt() {
1035         final AbstractConfiguration c = setUpTestConfiguration();
1036         config.addConfiguration(c, TEST_NAME);
1037         assertSame(c, config.removeConfigurationAt(0));
1038         checkRemoveConfig(c);
1039     }
1040 
1041     /**
1042      * Tests removing a configuration that was not added prior.
1043      */
1044     @Test
1045     void testRemoveNonContainedConfiguration() {
1046         assertFalse(config.removeConfiguration(setUpTestConfiguration()));
1047         listener.checkEvent(0, 0);
1048     }
1049 
1050     /**
1051      * Tests whether write access to the conversion expression engine is synchronized.
1052      */
1053     @Test
1054     void testSetConversionExpressionEngineSynchronized() {
1055         final SynchronizerTestImpl sync = setUpSynchronizerTest();
1056         config.setConversionExpressionEngine(new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS));
1057         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
1058         checkCombinedRootNotConstructed();
1059     }
1060 
1061     /**
1062      * Tests if setting a node combiner causes an invalidation.
1063      */
1064     @Test
1065     void testSetNodeCombiner() {
1066         final NodeCombiner combiner = new UnionCombiner();
1067         config.setNodeCombiner(combiner);
1068         assertSame(combiner, config.getNodeCombiner());
1069         listener.checkEvent(1, 0);
1070     }
1071 
1072     /**
1073      * Tests whether setNodeCombiner() is correctly synchronized.
1074      */
1075     @Test
1076     void testSetNodeCombinerSynchronized() {
1077         final SynchronizerTestImpl sync = setUpSynchronizerTest();
1078         config.setNodeCombiner(new UnionCombiner());
1079         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
1080         checkCombinedRootNotConstructed();
1081     }
1082 
1083     /**
1084      * Tests setting a null node combiner. This should cause an exception.
1085      */
1086     @Test
1087     void testSetNullNodeCombiner() {
1088         assertThrows(IllegalArgumentException.class, () -> config.setNodeCombiner(null));
1089     }
1090 
1091     /**
1092      * Tests whether a sub configuration survives updates of its parent.
1093      */
1094     @Test
1095     void testSubConfigurationWithUpdates() {
1096         final AbstractConfiguration srcConfig = setUpSubConfigTest();
1097         final HierarchicalConfiguration<ImmutableNode> sub = config.configurationAt(SUB_KEY, true);
1098         assertTrue(sub.getBoolean(TEST_KEY));
1099         srcConfig.setProperty(TEST_KEY, Boolean.FALSE);
1100         assertFalse(sub.getBoolean(TEST_KEY));
1101         assertFalse(config.getBoolean(SUB_KEY + '.' + TEST_KEY));
1102     }
1103 
1104     /**
1105      * Tests if an update of a contained configuration leeds to an invalidation of the combined configuration.
1106      */
1107     @Test
1108     void testUpdateContainedConfiguration() {
1109         final AbstractConfiguration c = setUpTestConfiguration();
1110         config.addConfiguration(c);
1111         c.addProperty("test.otherTest", "yes");
1112         assertEquals("yes", config.getString("test.otherTest"));
1113         listener.checkEvent(2, 0);
1114     }
1115 }