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     public static class ThreadLookup implements Lookup {
149         private static final ThreadLocal<String> id = new ThreadLocal<>();
150 
151         public static void setId(final String value) {
152             id.set(value);
153         }
154 
155         public ThreadLookup() {
156         }
157 
158         @Override
159         public String lookup(final String key) {
160             if (key == null || !key.equals("Id")) {
161                 return null;
162             }
163             final String value = System.getProperty("Id");
164             if (value != null) {
165                 return value;
166             }
167             return id.get();
168 
169         }
170     }
171 
172     private static final String PATTERN = "${sys:Id}";
173     private static final String PATTERN1 = "target/test-classes/testMultiConfiguration_${sys:Id}.xml";
174 
175     private static final String DEFAULT_FILE = "target/test-classes/testMultiConfiguration_default.xml";
176 
177     private static final File MULTI_TENENT_FILE = ConfigurationAssert.getTestFile("testMultiTenentConfigurationBuilder4.xml");
178 
179     private static final File MULTI_DYNAMIC_FILE = ConfigurationAssert.getTestFile("testMultiTenentConfigurationBuilder5.xml");
180 
181     /** Constant for the number of test threads. */
182     private static final int THREAD_COUNT = 3;
183 
184     /** Constant for the number of loops in the multi-thread tests. */
185     private static final int LOOP_COUNT = 100;
186 
187     /** A helper object for creating builder parameters. */
188     private static Parameters parameters;
189 
190     @BeforeAll
191     public static void setUpOnce() {
192         parameters = new Parameters();
193     }
194 
195     /** A folder for temporary files. */
196     @TempDir
197     public File tempFolder;
198 
199     private void copyFile(final File input, final File output) throws IOException {
200         FileUtils.copyFile(input, output, StandardCopyOption.REPLACE_EXISTING);
201         // On Windows, the last modified time is copied by default. Change the last modified time manually.
202         output.setLastModified(System.currentTimeMillis());
203     }
204 
205     /**
206      * Prepares a test for calling the Synchronizer. This method creates a test Synchronizer, installs it at the
207      * configuration and returns it.
208      *
209      * @param config the configuration
210      * @return the test Synchronizer
211      */
212     private SynchronizerTestImpl prepareSynchronizerTest(final Configuration config) {
213         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
214         config.setSynchronizer(sync);
215         config.lock(LockMode.READ);
216         config.unlock(LockMode.READ); // ensure that root node is constructed
217         sync.clear();
218         return sync;
219     }
220 
221     /**
222      * Tests whether adding a configuration is synchronized.
223      */
224     @Test
225     public void testAddConfigurationSynchronized() {
226         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
227         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
228         config.addConfiguration(new PropertiesConfiguration());
229         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
230     }
231 
232     @Test
233     public void testConcurrentGetAndReload() throws Exception {
234         System.getProperties().remove("Id");
235         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
236         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
237         final CombinedConfiguration config = builder.getConfiguration();
238 
239         assertEquals("50", config.getString("rowsPerPage"));
240         final Thread[] testThreads = new Thread[THREAD_COUNT];
241         final int[] failures = new int[THREAD_COUNT];
242 
243         for (int i = 0; i < testThreads.length; ++i) {
244             testThreads[i] = new ReloadThread(builder, failures, i, LOOP_COUNT, false, null, "50");
245             testThreads[i].start();
246         }
247 
248         int totalFailures = 0;
249         for (int i = 0; i < testThreads.length; ++i) {
250             testThreads[i].join();
251             totalFailures += failures[i];
252         }
253         assertEquals(0, totalFailures);
254     }
255 
256     @Test
257     public void testConcurrentGetAndReload2() throws Exception {
258         System.getProperties().remove("Id");
259         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
260         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
261         final CombinedConfiguration config = builder.getConfiguration();
262 
263         assertEquals("50", config.getString("rowsPerPage"));
264 
265         final Thread[] testThreads = new Thread[THREAD_COUNT];
266         final int[] failures = new int[THREAD_COUNT];
267         System.setProperty("Id", "2002");
268         assertEquals("25", config.getString("rowsPerPage"));
269         for (int i = 0; i < testThreads.length; ++i) {
270             testThreads[i] = new ReloadThread(builder, failures, i, LOOP_COUNT, false, null, "25");
271             testThreads[i].start();
272         }
273 
274         int totalFailures = 0;
275         for (int i = 0; i < testThreads.length; ++i) {
276             testThreads[i].join();
277             totalFailures += failures[i];
278         }
279         System.getProperties().remove("Id");
280         assertEquals(0, totalFailures);
281     }
282 
283     @Test
284     public void testConcurrentGetAndReloadFile() throws Exception {
285         final int threadCount = 25;
286         System.getProperties().remove("Id");
287         System.setProperty("TemporaryFolder", tempFolder.getAbsolutePath());
288         // create a new configuration
289         File input = new File("target/test-classes/testMultiDynamic_default.xml");
290         final File output = newFile("testMultiDynamic_default.xml", tempFolder);
291         output.delete();
292         output.getParentFile().mkdir();
293         copyFile(input, output);
294 
295         final ReloadingCombinedConfigurationBuilder builder = new ReloadingCombinedConfigurationBuilder();
296         builder.configure(parameters.combined().setSynchronizer(new ReadWriteSynchronizer())
297             .setDefinitionBuilderParameters(new FileBasedBuilderParametersImpl().setFile(MULTI_DYNAMIC_FILE)).registerChildDefaultsHandler(
298                 FileBasedBuilderProperties.class, new CopyObjectDefaultHandler(new FileBasedBuilderParametersImpl().setReloadingRefreshDelay(1L))));
299         CombinedConfiguration config = builder.getConfiguration();
300         assertEquals("ID0001", config.getString("Product/FIIndex/FI[@id='123456781']"));
301 
302         final ReaderThread[] testThreads = new ReaderThread[threadCount];
303         for (int i = 0; i < testThreads.length; ++i) {
304             testThreads[i] = new ReaderThread(builder);
305             testThreads[i].start();
306         }
307 
308         builder.getReloadingController().checkForReloading(null);
309         Thread.sleep(2000);
310 
311         input = new File("target/test-classes/testMultiDynamic_default2.xml");
312         copyFile(input, output);
313 
314         Thread.sleep(2000);
315         assertTrue(builder.getReloadingController().checkForReloading(null));
316         config = builder.getConfiguration();
317         final String id = config.getString("Product/FIIndex/FI[@id='123456782']");
318         assertNotNull(id);
319         final String rows = config.getString("rowsPerPage");
320         assertEquals("25", rows);
321 
322         for (final ReaderThread testThread : testThreads) {
323             testThread.shutdown();
324             testThread.join();
325         }
326         for (final ReaderThread testThread : testThreads) {
327             assertFalse(testThread.failed());
328         }
329         assertEquals("ID0002", config.getString("Product/FIIndex/FI[@id='123456782']"));
330         output.delete();
331     }
332 
333     @Test
334     public void testConcurrentGetAndReloadMultipleClients() throws Exception {
335         System.getProperties().remove("Id");
336         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
337         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
338         final CombinedConfiguration config = builder.getConfiguration();
339 
340         assertEquals("50", config.getString("rowsPerPage"));
341 
342         final Thread[] testThreads = new Thread[THREAD_COUNT];
343         final int[] failures = new int[THREAD_COUNT];
344         final String[] ids = {null, "2002", "3001", "3002", "3003"};
345         final String[] expected = {"50", "25", "15", "25", "50"};
346         for (int i = 0; i < testThreads.length; ++i) {
347             testThreads[i] = new ReloadThread(builder, failures, i, LOOP_COUNT, true, ids[i], expected[i]);
348             testThreads[i].start();
349         }
350 
351         int totalFailures = 0;
352         for (int i = 0; i < testThreads.length; ++i) {
353             testThreads[i].join();
354             totalFailures += failures[i];
355         }
356         System.getProperties().remove("Id");
357         if (totalFailures != 0) {
358             System.out.println("Failures:");
359             for (int i = 0; i < testThreads.length; ++i) {
360                 System.out.println("Thread " + i + " " + failures[i]);
361             }
362         }
363         assertEquals(0, totalFailures);
364     }
365 
366     @Test
367     public void testConfiguration() throws Exception {
368         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
369         final DefaultListDelimiterHandler listHandler = new DefaultListDelimiterHandler(',');
370         config.setListDelimiterHandler(listHandler);
371         final XPathExpressionEngine engine = new XPathExpressionEngine();
372         config.setExpressionEngine(engine);
373         config.setKeyPattern(PATTERN);
374         final ConfigurationBuilder<XMLConfiguration> multiBuilder = new MultiFileConfigurationBuilder<>(XMLConfiguration.class)
375             .configure(parameters.multiFile().setFilePattern(PATTERN1).setPrefixLookups(ConfigurationInterpolator.getDefaultPrefixLookups())
376                 .setManagedBuilderParameters(parameters.xml().setExpressionEngine(engine).setListDelimiterHandler(listHandler)));
377         final BuilderConfigurationWrapperFactory wrapFactory = new BuilderConfigurationWrapperFactory();
378         config.addConfiguration(wrapFactory.createBuilderConfigurationWrapper(HierarchicalConfiguration.class, multiBuilder), "Multi");
379         final XMLConfiguration xml = new XMLConfiguration();
380         xml.setExpressionEngine(engine);
381         final FileHandler handler = new FileHandler(xml);
382         handler.setFile(new File(DEFAULT_FILE));
383         handler.load();
384         config.addConfiguration(xml, "Default");
385 
386         verify("1001", config, 15);
387         verify("1002", config, 25);
388         verify("1003", config, 35);
389         verify("1004", config, 50);
390         assertEquals("a,b,c", config.getString("split/list3/@values"));
391         assertEquals(0, config.getMaxIndex("split/list3/@values"));
392         assertEquals("a\\,b\\,c", config.getString("split/list4/@values"));
393         assertEquals("OK-1", config.getString("buttons/name"));
394         assertEquals(3, config.getMaxIndex("buttons/name"));
395         assertEquals("a\\,b\\,c", config.getString("split/list2"));
396         assertEquals(18, config.size());
397         config.addProperty("listDelimiterTest", "1,2,3");
398         assertEquals("1", config.getString("listDelimiterTest"));
399     }
400 
401     /**
402      * Tests whether querying a configuration by index is synchronized.
403      */
404     @Test
405     public void testGetConfigurationByIdxSynchronized() {
406         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
407         final Configuration child = new PropertiesConfiguration();
408         config.addConfiguration(child);
409         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
410         assertSame(child, config.getConfiguration(0));
411         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
412     }
413 
414     /**
415      * Tests whether querying a configuration by name is synchronized.
416      */
417     @Test
418     public void testGetConfigurationByNameSynchronized() {
419         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
420         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
421         assertNull(config.getConfiguration("unknown config"));
422         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
423     }
424 
425     /**
426      * Tests whether querying the set of configuration names is synchronized.
427      */
428     @Test
429     public void testGetConfigurationNamesSynchronized() {
430         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
431         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
432         config.getConfigurationNames();
433         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
434     }
435 
436     /**
437      * Tests whether querying the number of configurations is synchronized.
438      */
439     @Test
440     public void testGetNumberOfConfigurationsSynchronized() {
441         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
442         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
443         config.getNumberOfConfigurations();
444         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
445     }
446 
447     /**
448      * Tests whether removing a child configuration is synchronized.
449      */
450     @Test
451     public void testRemoveConfigurationSynchronized() {
452         final DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
453         final String configName = "testConfig";
454         config.addConfiguration(new PropertiesConfiguration(), configName);
455         final SynchronizerTestImpl sync = prepareSynchronizerTest(config);
456         config.removeConfiguration(configName);
457         sync.verifyContains(Methods.BEGIN_WRITE);
458     }
459 
460     /**
461      * Tests whether a configuration can be updated.
462      */
463     @Test
464     public void testUpdateConfiguration() throws ConfigurationException {
465         System.getProperties().remove("Id");
466         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
467         builder.configure(parameters.fileBased().setFile(MULTI_TENENT_FILE).setSynchronizer(new ReadWriteSynchronizer()));
468         final CombinedConfiguration config = builder.getConfiguration();
469         config.getConfiguration(1).setProperty("rowsPerPage", "25");
470         assertEquals("25", config.getString("rowsPerPage"));
471     }
472 
473     private void verify(final String key, final DynamicCombinedConfiguration config, final int rows) {
474         System.setProperty("Id", key);
475         assertEquals(config.getInt("rowsPerPage"), rows);
476     }
477 }