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  
18  package org.apache.commons.configuration2;
19  
20  import static org.apache.commons.configuration2.TempDirUtils.newFile;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertSame;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  
28  import java.io.File;
29  import java.io.IOException;
30  import java.nio.file.StandardCopyOption;
31  import java.util.Random;
32  
33  import org.apache.commons.configuration2.SynchronizerTestImpl.Methods;
34  import org.apache.commons.configuration2.builder.BuilderConfigurationWrapperFactory;
35  import org.apache.commons.configuration2.builder.ConfigurationBuilder;
36  import org.apache.commons.configuration2.builder.CopyObjectDefaultHandler;
37  import org.apache.commons.configuration2.builder.FileBasedBuilderParametersImpl;
38  import org.apache.commons.configuration2.builder.FileBasedBuilderProperties;
39  import org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder;
40  import org.apache.commons.configuration2.builder.combined.MultiFileConfigurationBuilder;
41  import org.apache.commons.configuration2.builder.combined.ReloadingCombinedConfigurationBuilder;
42  import org.apache.commons.configuration2.builder.fluent.Parameters;
43  import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
44  import org.apache.commons.configuration2.ex.ConfigurationException;
45  import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
46  import org.apache.commons.configuration2.interpol.Lookup;
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.tree.xpath.XPathExpressionEngine;
51  import org.apache.commons.io.FileUtils;
52  import org.junit.jupiter.api.BeforeAll;
53  import org.junit.jupiter.api.Test;
54  import org.junit.jupiter.api.io.TempDir;
55  
56  public class TestDynamicCombinedConfiguration {
57      private final class ReaderThread extends Thread {
58          private volatile boolean running = true;
59          private volatile boolean failed;
60          private final CombinedConfigurationBuilder builder;
61          private final Random random;
62  
63          public ReaderThread(final CombinedConfigurationBuilder b) {
64              builder = b;
65              random = new Random();
66          }
67  
68          public boolean failed() {
69              return failed;
70          }
71  
72          @Override
73          public void run() {
74              try {
75                  while (running) {
76                      final CombinedConfiguration combined = builder.getConfiguration();
77                      final String bcId = combined.getString("Product/FIIndex/FI[@id='123456781']");
78                      if ("ID0001".equalsIgnoreCase(bcId)) {
79                          if (failed) {
80                              System.out.println("Thread failed, but recovered");
81                          }
82                          failed = false;
83                      } else {
84                          failed = true;
85                      }
86                      final int sleepTime = random.nextInt(75);
87                      Thread.sleep(sleepTime);
88                  }
89              } catch (final ConfigurationException cex) {
90                  failed = true;
91              } catch (final InterruptedException iex) {
92                  Thread.currentThread().interrupt();
93              }
94          }
95  
96          public void shutdown() {
97              running = false;
98              interrupt();
99          }
100 
101     }
102     private static final class ReloadThread extends Thread {
103         private final CombinedConfigurationBuilder builder;
104         private final int[] failures;
105         private final int index;
106         private final int count;
107         private final String expected;
108         private final String id;
109         private final boolean useId;
110         private final Random random;
111 
112         ReloadThread(final CombinedConfigurationBuilder b, final int[] failures, final int index, final int count, final boolean useId, final String id,
113             final String expected) {
114             builder = b;
115             this.failures = failures;
116             this.index = index;
117             this.count = count;
118             this.expected = expected;
119             this.id = id;
120             this.useId = useId;
121             random = new Random();
122         }
123 
124         @Override
125         public void run() {
126             failures[index] = 0;
127 
128             if (useId) {
129                 ThreadLookup.setId(id);
130             }
131             for (int i = 0; i < count; i++) {
132                 try {
133                     if (random.nextBoolean()) {
134                         // simulate a reload
135                         builder.resetResult();
136                     }
137                     final CombinedConfiguration combined = builder.getConfiguration();
138                     final String value = combined.getString("rowsPerPage", null);
139                     if (value == null || !value.equals(expected)) {
140                         ++failures[index];
141                     }
142                 } catch (final Exception ex) {
143                     ++failures[index];
144                 }
145             }
146         }
147     }
148 
149     public static class ThreadLookup implements Lookup {
150 
151         private static final ThreadLocal<String> ID = new ThreadLocal<>();
152 
153         public static void setId(final String value) {
154             ID.set(value);
155         }
156 
157         public ThreadLookup() {
158         }
159 
160         @Override
161         public String lookup(final String key) {
162             if (key == null || !key.equals("Id")) {
163                 return null;
164             }
165             final String value = System.getProperty("Id");
166             if (value != null) {
167                 return value;
168             }
169             return ID.get();
170 
171         }
172     }
173 
174     private static final String PATTERN = "${sys:Id}";
175     private static final String PATTERN1 = "target/test-classes/testMultiConfiguration_${sys:Id}.xml";
176 
177     private static final String DEFAULT_FILE = "target/test-classes/testMultiConfiguration_default.xml";
178 
179     private static final File MULTI_TENENT_FILE = ConfigurationAssert.getTestFile("testMultiTenentConfigurationBuilder4.xml");
180 
181     private static final File MULTI_DYNAMIC_FILE = ConfigurationAssert.getTestFile("testMultiTenentConfigurationBuilder5.xml");
182 
183     /** Constant for the number of test threads. */
184     private static final int THREAD_COUNT = 3;
185 
186     /** Constant for the number of loops in the multi-thread tests. */
187     private static final int LOOP_COUNT = 100;
188 
189     /** A helper object for creating builder parameters. */
190     private static Parameters parameters;
191 
192     @BeforeAll
193     public static void setUpOnce() {
194         parameters = new Parameters();
195     }
196 
197     /** A folder for temporary files. */
198     @TempDir
199     public File tempFolder;
200 
201     private void copyFile(final File input, final File output) throws IOException {
202         FileUtils.copyFile(input, output, StandardCopyOption.REPLACE_EXISTING);
203         // On Windows, the last modified time is copied by default. Change the last modified time manually.
204         output.setLastModified(System.currentTimeMillis());
205     }
206 
207     /**
208      * Prepares a test for calling the Synchronizer. This method creates a test Synchronizer, installs it at the
209      * configuration and returns it.
210      *
211      * @param config the configuration
212      * @return the test Synchronizer
213      */
214     private SynchronizerTestImpl prepareSynchronizerTest(final Configuration config) {
215         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
216         config.setSynchronizer(sync);
217         config.lock(LockMode.READ);
218         config.unlock(LockMode.READ); // ensure that root node is constructed
219         sync.clear();
220         return sync;
221     }
222 
223     /**
224      * Tests whether adding a configuration is synchronized.
225      */
226     @Test
227     public void testAddConfigurationSynchronized() {
228         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
229         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
230         config.addConfiguration(new PropertiesConfiguration());
231         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
232     }
233 
234     @Test
235     public void testConcurrentGetAndReload() throws Exception {
236         System.getProperties().remove("Id");
237         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
238         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
239         final CombinedConfiguration config = builder.getConfiguration();
240 
241         assertEquals("50", config.getString("rowsPerPage"));
242         final Thread[] testThreads = new Thread[THREAD_COUNT];
243         final int[] failures = new int[THREAD_COUNT];
244 
245         for (int i = 0; i < testThreads.length; ++i) {
246             testThreads[i] = new ReloadThread(builder, failures, i, LOOP_COUNT, false, null, "50");
247             testThreads[i].start();
248         }
249 
250         int totalFailures = 0;
251         for (int i = 0; i < testThreads.length; ++i) {
252             testThreads[i].join();
253             totalFailures += failures[i];
254         }
255         assertEquals(0, totalFailures);
256     }
257 
258     @Test
259     public void testConcurrentGetAndReload2() throws Exception {
260         System.getProperties().remove("Id");
261         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
262         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
263         final CombinedConfiguration config = builder.getConfiguration();
264 
265         assertEquals("50", config.getString("rowsPerPage"));
266 
267         final Thread[] testThreads = new Thread[THREAD_COUNT];
268         final int[] failures = new int[THREAD_COUNT];
269         System.setProperty("Id", "2002");
270         assertEquals("25", config.getString("rowsPerPage"));
271         for (int i = 0; i < testThreads.length; ++i) {
272             testThreads[i] = new ReloadThread(builder, failures, i, LOOP_COUNT, false, null, "25");
273             testThreads[i].start();
274         }
275 
276         int totalFailures = 0;
277         for (int i = 0; i < testThreads.length; ++i) {
278             testThreads[i].join();
279             totalFailures += failures[i];
280         }
281         System.getProperties().remove("Id");
282         assertEquals(0, totalFailures);
283     }
284 
285     @Test
286     public void testConcurrentGetAndReloadFile() throws Exception {
287         final int threadCount = 25;
288         System.getProperties().remove("Id");
289         System.setProperty("TemporaryFolder", tempFolder.getAbsolutePath());
290         // create a new configuration
291         File input = new File("target/test-classes/testMultiDynamic_default.xml");
292         final File output = newFile("testMultiDynamic_default.xml", tempFolder);
293         output.delete();
294         output.getParentFile().mkdir();
295         copyFile(input, output);
296 
297         final ReloadingCombinedConfigurationBuilder builder = new ReloadingCombinedConfigurationBuilder();
298         builder.configure(parameters.combined().setSynchronizer(new ReadWriteSynchronizer())
299             .setDefinitionBuilderParameters(new FileBasedBuilderParametersImpl().setFile(MULTI_DYNAMIC_FILE)).registerChildDefaultsHandler(
300                 FileBasedBuilderProperties.class, new CopyObjectDefaultHandler(new FileBasedBuilderParametersImpl().setReloadingRefreshDelay(1L))));
301         CombinedConfiguration config = builder.getConfiguration();
302         assertEquals("ID0001", config.getString("Product/FIIndex/FI[@id='123456781']"));
303 
304         final ReaderThread[] testThreads = new ReaderThread[threadCount];
305         for (int i = 0; i < testThreads.length; ++i) {
306             testThreads[i] = new ReaderThread(builder);
307             testThreads[i].start();
308         }
309 
310         builder.getReloadingController().checkForReloading(null);
311         Thread.sleep(2000);
312 
313         input = new File("target/test-classes/testMultiDynamic_default2.xml");
314         copyFile(input, output);
315 
316         Thread.sleep(2000);
317         assertTrue(builder.getReloadingController().checkForReloading(null));
318         config = builder.getConfiguration();
319         final String id = config.getString("Product/FIIndex/FI[@id='123456782']");
320         assertNotNull(id);
321         final String rows = config.getString("rowsPerPage");
322         assertEquals("25", rows);
323 
324         for (final ReaderThread testThread : testThreads) {
325             testThread.shutdown();
326             testThread.join();
327         }
328         for (final ReaderThread testThread : testThreads) {
329             assertFalse(testThread.failed());
330         }
331         assertEquals("ID0002", config.getString("Product/FIIndex/FI[@id='123456782']"));
332         output.delete();
333     }
334 
335     @Test
336     public void testConcurrentGetAndReloadMultipleClients() throws Exception {
337         System.getProperties().remove("Id");
338         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
339         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
340         final CombinedConfiguration config = builder.getConfiguration();
341 
342         assertEquals("50", config.getString("rowsPerPage"));
343 
344         final Thread[] testThreads = new Thread[THREAD_COUNT];
345         final int[] failures = new int[THREAD_COUNT];
346         final String[] ids = {null, "2002", "3001", "3002", "3003"};
347         final String[] expected = {"50", "25", "15", "25", "50"};
348         for (int i = 0; i < testThreads.length; ++i) {
349             testThreads[i] = new ReloadThread(builder, failures, i, LOOP_COUNT, true, ids[i], expected[i]);
350             testThreads[i].start();
351         }
352 
353         int totalFailures = 0;
354         for (int i = 0; i < testThreads.length; ++i) {
355             testThreads[i].join();
356             totalFailures += failures[i];
357         }
358         System.getProperties().remove("Id");
359         if (totalFailures != 0) {
360             System.out.println("Failures:");
361             for (int i = 0; i < testThreads.length; ++i) {
362                 System.out.println("Thread " + i + " " + failures[i]);
363             }
364         }
365         assertEquals(0, totalFailures);
366     }
367 
368     @Test
369     public void testConfiguration() throws Exception {
370         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
371         final DefaultListDelimiterHandler listHandler = new DefaultListDelimiterHandler(',');
372         config.setListDelimiterHandler(listHandler);
373         final XPathExpressionEngine engine = new XPathExpressionEngine();
374         config.setExpressionEngine(engine);
375         config.setKeyPattern(PATTERN);
376         final ConfigurationBuilder<XMLConfiguration> multiBuilder = new MultiFileConfigurationBuilder<>(XMLConfiguration.class)
377             .configure(parameters.multiFile().setFilePattern(PATTERN1).setPrefixLookups(ConfigurationInterpolator.getDefaultPrefixLookups())
378                 .setManagedBuilderParameters(parameters.xml().setExpressionEngine(engine).setListDelimiterHandler(listHandler)));
379         final BuilderConfigurationWrapperFactory wrapFactory = new BuilderConfigurationWrapperFactory();
380         config.addConfiguration(wrapFactory.createBuilderConfigurationWrapper(HierarchicalConfiguration.class, multiBuilder), "Multi");
381         final XMLConfiguration xml = new XMLConfiguration();
382         xml.setExpressionEngine(engine);
383         final FileHandler handler = new FileHandler(xml);
384         handler.setFile(new File(DEFAULT_FILE));
385         handler.load();
386         config.addConfiguration(xml, "Default");
387 
388         verify("1001", config, 15);
389         verify("1002", config, 25);
390         verify("1003", config, 35);
391         verify("1004", config, 50);
392         assertEquals("a,b,c", config.getString("split/list3/@values"));
393         assertEquals(0, config.getMaxIndex("split/list3/@values"));
394         assertEquals("a\\,b\\,c", config.getString("split/list4/@values"));
395         assertEquals("OK-1", config.getString("buttons/name"));
396         assertEquals(3, config.getMaxIndex("buttons/name"));
397         assertEquals("a\\,b\\,c", config.getString("split/list2"));
398         assertEquals(18, config.size());
399         config.addProperty("listDelimiterTest", "1,2,3");
400         assertEquals("1", config.getString("listDelimiterTest"));
401     }
402 
403     /**
404      * Tests whether querying a configuration by index is synchronized.
405      */
406     @Test
407     public void testGetConfigurationByIdxSynchronized() {
408         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
409         final Configuration child = new PropertiesConfiguration();
410         config.addConfiguration(child);
411         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
412         assertSame(child, config.getConfiguration(0));
413         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
414     }
415 
416     /**
417      * Tests whether querying a configuration by name is synchronized.
418      */
419     @Test
420     public void testGetConfigurationByNameSynchronized() {
421         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
422         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
423         assertNull(config.getConfiguration("unknown config"));
424         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
425     }
426 
427     /**
428      * Tests whether querying the set of configuration names is synchronized.
429      */
430     @Test
431     public void testGetConfigurationNamesSynchronized() {
432         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
433         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
434         config.getConfigurationNames();
435         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
436     }
437 
438     /**
439      * Tests whether querying the number of configurations is synchronized.
440      */
441     @Test
442     public void testGetNumberOfConfigurationsSynchronized() {
443         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
444         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
445         config.getNumberOfConfigurations();
446         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
447     }
448 
449     /**
450      * Tests whether removing a child configuration is synchronized.
451      */
452     @Test
453     public void testRemoveConfigurationSynchronized() {
454         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
455         final String configName = "testConfig";
456         config.addConfiguration(new PropertiesConfiguration(), configName);
457         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
458         config.removeConfiguration(configName);
459         sync.verifyContains(Methods.BEGIN_WRITE);
460     }
461 
462     /**
463      * Tests whether a configuration can be updated.
464      */
465     @Test
466     public void testUpdateConfiguration() throws ConfigurationException {
467         System.getProperties().remove("Id");
468         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
469         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
470         final CombinedConfiguration config = builder.getConfiguration();
471         config.getConfiguration(1).setProperty("rowsPerPage", "25");
472         assertEquals("25", config.getString("rowsPerPage"));
473     }
474 
475     private void verify(final String key, final DynamicCombinedConfiguration config, final int rows) {
476         System.setProperty("Id", key);
477         assertEquals(config.getInt("rowsPerPage"), rows);
478     }
479 }