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.builder.combined;
18  
19  import static org.apache.commons.configuration2.TempDirUtils.newFile;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertTrue;
22  
23  import java.io.File;
24  import java.io.FileWriter;
25  import java.io.IOException;
26  import java.io.PrintWriter;
27  import java.text.MessageFormat;
28  
29  import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
30  import org.apache.commons.configuration2.CombinedConfiguration;
31  import org.apache.commons.configuration2.Configuration;
32  import org.apache.commons.configuration2.XMLConfiguration;
33  import org.apache.commons.configuration2.builder.BasicBuilderParameters;
34  import org.apache.commons.configuration2.builder.BasicBuilderProperties;
35  import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
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.ReloadingDetectorFactory;
40  import org.apache.commons.configuration2.builder.ReloadingFileBasedConfigurationBuilder;
41  import org.apache.commons.configuration2.builder.fluent.Parameters;
42  import org.apache.commons.configuration2.ex.ConfigurationException;
43  import org.apache.commons.configuration2.io.FileHandler;
44  import org.apache.commons.configuration2.reloading.AlwaysReloadingDetector;
45  import org.apache.commons.configuration2.reloading.RandomReloadingDetector;
46  import org.apache.commons.configuration2.sync.ReadWriteSynchronizer;
47  import org.apache.commons.configuration2.sync.Synchronizer;
48  import org.apache.commons.configuration2.tree.MergeCombiner;
49  import org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine;
50  import org.junit.jupiter.api.BeforeEach;
51  import org.junit.jupiter.api.Test;
52  import org.junit.jupiter.api.io.TempDir;
53  
54  /**
55   * Test class for {@code ReloadingCombinedConfigurationBuilder} which actually accesses files to be reloaded.
56   */
57  public class TestReloadingCombinedConfigurationBuilderFileBased {
58  
59      /**
60       * A test builder class which always returns the same configuration.
61       */
62      private static final class ConstantConfigurationBuilder extends BasicConfigurationBuilder<BaseHierarchicalConfiguration> {
63          private final BaseHierarchicalConfiguration configuration;
64  
65          public ConstantConfigurationBuilder(final BaseHierarchicalConfiguration conf) {
66              super(BaseHierarchicalConfiguration.class);
67              configuration = conf;
68          }
69  
70          @Override
71          public BaseHierarchicalConfiguration getConfiguration() throws ConfigurationException {
72              return configuration;
73          }
74      }
75  
76      /**
77       * A thread class for testing concurrent reload operations.
78       */
79      private static final class ReloadThread extends Thread {
80  
81          /** The builder to be queried. */
82          private final ReloadingCombinedConfigurationBuilder builder;
83  
84          /** An array for reporting failures. */
85          private final int[] failures;
86  
87          /** The index of this thread in the array with failures. */
88          private final int index;
89  
90          /** The number of test operations. */
91          private final int count;
92  
93          ReloadThread(final ReloadingCombinedConfigurationBuilder bldr, final int[] failures, final int index, final int count) {
94              builder = bldr;
95              this.failures = failures;
96              this.index = index;
97              this.count = count;
98          }
99  
100         @Override
101         public void run() {
102             failures[index] = 0;
103             for (int i = 0; i < count; i++) {
104                 try {
105                     builder.getReloadingController().checkForReloading(null);
106                     final String value = builder.getConfiguration().getString("/property[@name='config']/@value");
107                     if (value == null || !value.equals("100")) {
108                         ++failures[index];
109                     }
110                 } catch (final Exception ex) {
111                     ++failures[index];
112                 }
113             }
114         }
115     }
116 
117     /** Constant for the prefix for XML configuration sources. */
118     private static final String PROP_SRC = "override.xml";
119 
120     /** Constant for the prefix of the reload property. */
121     private static final String PROP_RELOAD = "default.xmlReload";
122 
123     /** Constant for content of a XML configuration for reload tests. */
124     private static final String RELOAD_CONTENT = "<config><default><xmlReload{1}>{0}</xmlReload{1}></default></config>";
125 
126     /**
127      * Adds a source for a configuration which can be reloaded to the definition configuration.
128      *
129      * @param config the definition configuration
130      * @param fileName the name of the file
131      */
132     private static void addReloadSource(final Configuration config, final String fileName) {
133         config.addProperty(PROP_SRC + "(-1)[@fileName]", fileName);
134         config.addProperty(PROP_SRC + "[@config-reload]", Boolean.TRUE);
135     }
136 
137     /**
138      * Returns the name of a test property.
139      *
140      * @param idx the index of the property
141      * @return the test property with this index
142      */
143     private static String testProperty(final int idx) {
144         return PROP_RELOAD + idx;
145     }
146 
147     /**
148      * Helper method for writing a file.
149      *
150      * @param file the file to be written
151      * @param content the file's content
152      * @throws IOException if an error occurs
153      */
154     private static void writeFile(final File file, final String content) throws IOException {
155         try (PrintWriter out = new PrintWriter(new FileWriter(file))) {
156             out.print(content);
157         }
158     }
159 
160     /** A folder for temporary files. */
161     @TempDir
162     public File tempFolder;
163 
164     /** A helper object for creating builder parameters. */
165     private Parameters parameters;
166 
167     /** The builder to be tested. */
168     private ReloadingCombinedConfigurationBuilder builder;
169 
170     /**
171      * Helper method for testing whether the builder's definition file can be reloaded. This method expects that the test
172      * builder has been fully initialized.
173      *
174      * @param defFile the path to the definition file
175      * @throws IOException if an I/O error occurs.
176      * @throws ConfigurationException if a configuration-related error occurs
177      * @throws InterruptedException if waiting is interrupted
178      */
179     private void checkReloadDefinitionFile(final File defFile) throws IOException, ConfigurationException, InterruptedException {
180         final File src1 = writeReloadFile(null, 1, 0);
181         final File src2 = writeReloadFile(null, 1, 1);
182         writeDefinitionFile(defFile, src1);
183         CombinedConfiguration config = builder.getConfiguration();
184         assertEquals(0, config.getInt(testProperty(1)));
185 
186         // No change definition file
187         boolean reloaded = false;
188         for (int attempts = 0; attempts < 50 && !reloaded; attempts++) {
189             writeDefinitionFile(defFile, src2);
190             reloaded = builder.getReloadingController().checkForReloading(null);
191             if (!reloaded) {
192                 Thread.sleep(100);
193             }
194         }
195         assertTrue(reloaded);
196         config = builder.getConfiguration();
197         assertEquals(1, config.getInt(testProperty(1)));
198     }
199 
200     @BeforeEach
201     public void setUp() throws Exception {
202         parameters = new Parameters();
203         builder = new ReloadingCombinedConfigurationBuilder();
204     }
205 
206     /**
207      * Tests concurrent access to a reloading builder for combined configurations.
208      */
209     @Test
210     void testConcurrentGetAndReload() throws Exception {
211         final int threadCount = 4;
212         final int loopCount = 100;
213         final ReloadingDetectorFactory detectorFactory = (handler, params) -> new RandomReloadingDetector();
214         final BaseHierarchicalConfiguration defConf = new BaseHierarchicalConfiguration();
215         defConf.addProperty("header.result.nodeCombiner[@config-class]", MergeCombiner.class.getName());
216         defConf.addProperty("header.result.expressionEngine[@config-class]", XPathExpressionEngine.class.getName());
217         addReloadSource(defConf, "configA.xml");
218         addReloadSource(defConf, "configB.xml");
219         final Synchronizer sync = new ReadWriteSynchronizer();
220         builder.configure(parameters.combined().setDefinitionBuilder(new ConstantConfigurationBuilder(defConf)).setSynchronizer(sync)
221             .registerChildDefaultsHandler(BasicBuilderProperties.class, new CopyObjectDefaultHandler(new BasicBuilderParameters().setSynchronizer(sync)))
222             .registerChildDefaultsHandler(FileBasedBuilderProperties.class,
223                 new CopyObjectDefaultHandler(new FileBasedBuilderParametersImpl().setReloadingDetectorFactory(detectorFactory))));
224 
225         assertEquals("100", builder.getConfiguration().getString("/property[@name='config']/@value"));
226 
227         final Thread[] testThreads = new Thread[threadCount];
228         final int[] failures = new int[threadCount];
229 
230         for (int i = 0; i < testThreads.length; ++i) {
231             testThreads[i] = new ReloadThread(builder, failures, i, loopCount);
232             testThreads[i].start();
233         }
234 
235         int totalFailures = 0;
236         for (int i = 0; i < testThreads.length; ++i) {
237             testThreads[i].join();
238             totalFailures += failures[i];
239         }
240         assertEquals(0, totalFailures);
241     }
242 
243     /**
244      * Tests whether the default definition builder is capable of detecting a change in the definition configuration.
245      */
246     @Test
247     void testReloadDefinitionFileDefaultBuilder() throws ConfigurationException, IOException, InterruptedException {
248         final File defFile = newFile(tempFolder);
249         builder.configure(parameters.combined().setDefinitionBuilderParameters(parameters.xml().setReloadingRefreshDelay(0L).setFile(defFile)));
250         checkReloadDefinitionFile(defFile);
251     }
252 
253     /**
254      * Tests whether a change in the definition file is detected and causes a reload if a specific builder for the
255      * definition configuration is provided.
256      */
257     @Test
258     void testReloadDefinitionFileExplicitBuilder() throws ConfigurationException, IOException, InterruptedException {
259         final File defFile = newFile(tempFolder);
260         builder.configure(parameters.combined().setDefinitionBuilder(
261             new ReloadingFileBasedConfigurationBuilder<>(XMLConfiguration.class).configure(parameters.xml().setReloadingRefreshDelay(0L).setFile(defFile))));
262         checkReloadDefinitionFile(defFile);
263     }
264 
265     /**
266      * Tests whether a changed file is detected on disk.
267      */
268     @Test
269     void testReloadFromFile() throws ConfigurationException, IOException {
270         final File xmlConf1 = writeReloadFile(null, 1, 0);
271         final File xmlConf2 = writeReloadFile(null, 2, 0);
272         final ReloadingDetectorFactory detectorFactory = (handler, params) -> new AlwaysReloadingDetector();
273         final BaseHierarchicalConfiguration defConf = new BaseHierarchicalConfiguration();
274         addReloadSource(defConf, xmlConf1.getAbsolutePath());
275         addReloadSource(defConf, xmlConf2.getAbsolutePath());
276         builder.configure(parameters.combined().setDefinitionBuilder(new ConstantConfigurationBuilder(defConf)).registerChildDefaultsHandler(
277             FileBasedBuilderProperties.class, new CopyObjectDefaultHandler(new FileBasedBuilderParametersImpl().setReloadingDetectorFactory(detectorFactory))));
278         CombinedConfiguration config = builder.getConfiguration();
279         assertEquals(0, config.getInt(testProperty(1)));
280         assertEquals(0, config.getInt(testProperty(2)));
281 
282         writeReloadFile(xmlConf1, 1, 1);
283         builder.getReloadingController().checkForReloading(null);
284         config = builder.getConfiguration();
285         assertEquals(1, config.getInt(testProperty(1)));
286         assertEquals(0, config.getInt(testProperty(2)));
287 
288         writeReloadFile(xmlConf2, 2, 2);
289         builder.getReloadingController().checkForReloading(null);
290         config = builder.getConfiguration();
291         assertEquals(1, config.getInt(testProperty(1)));
292         assertEquals(2, config.getInt(testProperty(2)));
293     }
294 
295     /**
296      * Writes a configuration definition file that refers to the specified file source.
297      *
298      * @param defFile the target definition file
299      * @param src the configuration source file to be referenced
300      * @throws ConfigurationException if an error occurs
301      */
302     private void writeDefinitionFile(final File defFile, final File src) throws ConfigurationException {
303         final XMLConfiguration defConf = new XMLConfiguration();
304         addReloadSource(defConf, src.getAbsolutePath());
305         new FileHandler(defConf).save(defFile);
306     }
307 
308     /**
309      * Writes a file for testing reload operations.
310      *
311      * @param f the file to be written or <strong>null</strong> for creating a new one
312      * @param tagIdx the index of the tag
313      * @param value the value of the reload test property
314      * @return the file that was written
315      * @throws IOException if an error occurs
316      */
317     private File writeReloadFile(final File f, final int tagIdx, final int value) throws IOException {
318         return writeReloadFile(f, MessageFormat.format(RELOAD_CONTENT, value, tagIdx));
319     }
320 
321     /**
322      * Helper method for writing a test file for reloading. The file will be created in the test directory. It is also
323      * scheduled for automatic deletion after the test.
324      *
325      * @param f the file to be written or <strong>null</strong> for creating a new one
326      * @param content the content of the file
327      * @return the {@code File} object for the test file
328      * @throws IOException if an error occurs
329      */
330     private File writeReloadFile(final File f, final String content) throws IOException {
331         final File file = f != null ? f : newFile(tempFolder);
332         writeFile(file, content);
333         return file;
334     }
335 }