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