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