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