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  
18  package org.apache.commons.configuration2;
19  
20  import static org.apache.commons.configuration2.TempDirUtils.newFile;
21  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertFalse;
24  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
25  import static org.junit.jupiter.api.Assertions.assertNotNull;
26  import static org.junit.jupiter.api.Assertions.assertNotSame;
27  import static org.junit.jupiter.api.Assertions.assertNull;
28  import static org.junit.jupiter.api.Assertions.assertSame;
29  import static org.junit.jupiter.api.Assertions.assertThrows;
30  import static org.junit.jupiter.api.Assertions.assertTrue;
31  
32  import java.beans.beancontext.BeanContextServicesSupport;
33  import java.beans.beancontext.BeanContextSupport;
34  import java.io.BufferedReader;
35  import java.io.ByteArrayInputStream;
36  import java.io.File;
37  import java.io.FileOutputStream;
38  import java.io.FileReader;
39  import java.io.FileWriter;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.OutputStream;
43  import java.io.Reader;
44  import java.io.StringReader;
45  import java.io.StringWriter;
46  import java.io.Writer;
47  import java.net.HttpURLConnection;
48  import java.net.URL;
49  import java.net.URLConnection;
50  import java.net.URLStreamHandler;
51  import java.nio.charset.StandardCharsets;
52  import java.nio.file.FileSystems;
53  import java.nio.file.Files;
54  import java.nio.file.Paths;
55  import java.util.ArrayDeque;
56  import java.util.ArrayList;
57  import java.util.Arrays;
58  import java.util.Collection;
59  import java.util.Collections;
60  import java.util.HashSet;
61  import java.util.Iterator;
62  import java.util.List;
63  import java.util.PriorityQueue;
64  import java.util.Properties;
65  import java.util.Set;
66  import java.util.TreeSet;
67  
68  import org.apache.commons.collections.IteratorUtils;
69  import org.apache.commons.configuration2.SynchronizerTestImpl.Methods;
70  import org.apache.commons.configuration2.builder.FileBasedBuilderParametersImpl;
71  import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
72  import org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder;
73  import org.apache.commons.configuration2.builder.fluent.Configurations;
74  import org.apache.commons.configuration2.builder.fluent.Parameters;
75  import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
76  import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
77  import org.apache.commons.configuration2.convert.LegacyListDelimiterHandler;
78  import org.apache.commons.configuration2.convert.ListDelimiterHandler;
79  import org.apache.commons.configuration2.event.ConfigurationEvent;
80  import org.apache.commons.configuration2.ex.ConfigurationDeniedException;
81  import org.apache.commons.configuration2.ex.ConfigurationException;
82  import org.apache.commons.configuration2.io.AbsoluteNameLocationStrategy;
83  import org.apache.commons.configuration2.io.BasePathLocationStrategy;
84  import org.apache.commons.configuration2.io.ClasspathLocationStrategy;
85  import org.apache.commons.configuration2.io.CombinedLocationStrategy;
86  import org.apache.commons.configuration2.io.DefaultFileSystem;
87  import org.apache.commons.configuration2.io.FileHandler;
88  import org.apache.commons.configuration2.io.FileLocatorUtils;
89  import org.apache.commons.configuration2.io.FileSystem;
90  import org.apache.commons.configuration2.io.FileSystemLocationStrategy;
91  import org.apache.commons.configuration2.io.HomeDirectoryLocationStrategy;
92  import org.apache.commons.configuration2.io.ProvidedURLLocationStrategy;
93  import org.apache.commons.lang3.mutable.MutableObject;
94  import org.junit.jupiter.api.BeforeEach;
95  import org.junit.jupiter.api.Test;
96  import org.junit.jupiter.api.io.TempDir;
97  import org.junit.jupiter.params.ParameterizedTest;
98  import org.junit.jupiter.params.provider.ValueSource;
99  import org.junitpioneer.jupiter.SetSystemProperty;
100 
101 /**
102  * Test for loading and saving properties files.
103  */
104 public class TestPropertiesConfiguration {
105 
106     /**
107      * A dummy layout implementation for checking whether certain methods are correctly called by the configuration.
108      */
109     static class DummyLayout extends PropertiesConfigurationLayout {
110 
111         /** Stores the number how often load() was called. */
112         private int loadCalls;
113 
114         @Override
115         public void load(final PropertiesConfiguration config, final Reader in) throws ConfigurationException {
116             loadCalls++;
117         }
118     }
119 
120     static class ExceptionList implements ConfigurationConsumer<ConfigurationException> {
121 
122         private final List<ConfigurationException> exceptions = new ArrayList<>();
123 
124         @Override
125         public void accept(final ConfigurationException t) throws ConfigurationException {
126             exceptions.add(t);
127         }
128 
129         List<ConfigurationException> getExceptions() {
130             return exceptions;
131         }
132 
133     }
134 
135     /**
136      * A mock implementation of a HttpURLConnection used for testing saving to a HTTP server.
137      */
138     static class MockHttpURLConnection extends HttpURLConnection {
139 
140         /** The response code to return. */
141         private final int returnCode;
142 
143         /** The output file. The output stream will point to this file. */
144         private final File outputFile;
145 
146         protected MockHttpURLConnection(final URL url, final int respCode, final File outFile) {
147             super(url);
148             returnCode = respCode;
149             outputFile = outFile;
150         }
151 
152         @Override
153         public void connect() throws IOException {
154         }
155 
156         @Override
157         public void disconnect() {
158         }
159 
160         @Override
161         public OutputStream getOutputStream() throws IOException {
162             return new FileOutputStream(outputFile);
163         }
164 
165         @Override
166         public int getResponseCode() throws IOException {
167             return returnCode;
168         }
169 
170         @Override
171         public boolean usingProxy() {
172             return false;
173         }
174     }
175 
176     /**
177      * A mock stream handler for working with the mock HttpURLConnection.
178      */
179     static class MockHttpURLStreamHandler extends URLStreamHandler {
180 
181         /** Stores the response code. */
182         private final int responseCode;
183 
184         /** Stores the output file. */
185         private final File outputFile;
186 
187         /** Stores the connection. */
188         private MockHttpURLConnection connection;
189 
190         public MockHttpURLStreamHandler(final int respCode, final File outFile) {
191             responseCode = respCode;
192             outputFile = outFile;
193         }
194 
195         public MockHttpURLConnection getMockConnection() {
196             return connection;
197         }
198 
199         @Override
200         protected URLConnection openConnection(final URL u) throws IOException {
201             connection = new MockHttpURLConnection(u, responseCode, outputFile);
202             return connection;
203         }
204     }
205 
206     /**
207      * A test PropertiesReader for testing whether a custom reader can be injected. This implementation creates a
208      * configurable number of synthetic test properties.
209      */
210     private static final class PropertiesReaderTestImpl extends PropertiesConfiguration.PropertiesReader {
211 
212         /** The number of test properties to be created. */
213         private final int maxProperties;
214 
215         /** The current number of properties. */
216         private int propertyCount;
217 
218         public PropertiesReaderTestImpl(final Reader reader, final int maxProps) {
219             super(reader);
220             maxProperties = maxProps;
221         }
222 
223         @Override
224         public String getPropertyName() {
225             return PROP_NAME + propertyCount;
226         }
227 
228         @Override
229         public String getPropertyValue() {
230             return PROP_VALUE + propertyCount;
231         }
232 
233         @Override
234         public boolean nextProperty() throws IOException {
235             propertyCount++;
236             return propertyCount <= maxProperties;
237         }
238     }
239 
240     /**
241      * A test PropertiesWriter for testing whether a custom writer can be injected. This implementation simply redirects all
242      * output into a test file.
243      */
244     private static final class PropertiesWriterTestImpl extends PropertiesConfiguration.PropertiesWriter {
245         public PropertiesWriterTestImpl(final ListDelimiterHandler handler) throws IOException {
246             super(new FileWriter(TEST_SAVE_PROPERTIES_FILE), handler);
247         }
248     }
249 
250     /**
251      * All URL schemes.
252      */
253     private static final String ALL_SCHEMES = "file,http,https,jar";
254 
255     private static final String KEY_SCHEMES = "org.apache.commons.configuration2.io.FileLocationStrategy.schemes";
256 
257     /** Constant for a test property name. */
258     private static final String PROP_NAME = "testProperty";
259 
260     /** Constant for a test property value. */
261     private static final String PROP_VALUE = "value";
262 
263     /** Constant for the line break character. */
264     private static final String CR = System.lineSeparator();
265 
266     /** The File that we test with */
267     private static final String TEST_PROPERTIES = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
268 
269     private static final String TEST_BASE_PATH = ConfigurationAssert.TEST_DIR.getAbsolutePath();
270 
271     private static final String TEST_BASE_PATH_2 = ConfigurationAssert.TEST_DIR.getParentFile().getAbsolutePath();
272 
273     private static final File TEST_SAVE_PROPERTIES_FILE = ConfigurationAssert.getOutFile("testsave.properties");
274 
275     /**
276      * Helper method for loading a configuration from a given file.
277      *
278      * @param pc the configuration to be loaded
279      * @param fileName the file name
280      * @return the file handler associated with the configuration
281      * @throws ConfigurationException if an error occurs
282      */
283     private static FileHandler load(final PropertiesConfiguration pc, final String fileName) throws ConfigurationException {
284         final FileHandler handler = new FileHandler(pc);
285         handler.setFileName(fileName);
286         handler.load();
287         return handler;
288     }
289 
290     /** The configuration to be tested. */
291     private PropertiesConfiguration conf;
292 
293     /** A folder for temporary files. */
294     @TempDir
295     public File tempFolder;
296 
297     /**
298      * Helper method for testing the content of a list with elements that contain backslashes.
299      *
300      * @param key the key
301      */
302     private void checkBackslashList(final String key) {
303         final Object prop = conf.getProperty("test." + key);
304         final List<?> list = assertInstanceOf(List.class, prop);
305         final String prefix = "\\\\" + key;
306         assertEquals(Arrays.asList(prefix + "a", prefix + "b"), list);
307     }
308 
309     /**
310      * Tests whether the data of a configuration that was copied into the test configuration is correctly saved.
311      *
312      * @param copyConf the copied configuration
313      * @throws ConfigurationException if an error occurs
314      */
315     private void checkCopiedConfig(final Configuration copyConf) throws ConfigurationException {
316         saveTestConfig();
317         final PropertiesConfiguration checkConf = new PropertiesConfiguration();
318         load(checkConf, TEST_SAVE_PROPERTIES_FILE.getAbsolutePath());
319         for (final Iterator<String> it = copyConf.getKeys(); it.hasNext();) {
320             final String key = it.next();
321             assertEquals(checkConf.getProperty(key), copyConf.getProperty(key), "Wrong value for property " + key);
322         }
323     }
324 
325     /**
326      * Checks for a property without a value.
327      *
328      * @param key the key to be checked
329      */
330     private void checkEmpty(final String key) {
331         final String empty = conf.getString(key);
332         assertNotNull(empty, "Property not found: " + key);
333         assertEquals("", empty, "Wrong value for property " + key);
334     }
335 
336     /**
337      * Helper method for testing a saved configuration. Reads in the file using a new instance and compares this instance
338      * with the original one.
339      *
340      * @return the newly created configuration instance
341      * @throws ConfigurationException if an error occurs
342      */
343     private PropertiesConfiguration checkSavedConfig() throws ConfigurationException {
344         final PropertiesConfiguration checkConfig = new PropertiesConfiguration();
345         checkConfig.setListDelimiterHandler(new LegacyListDelimiterHandler(','));
346         load(checkConfig, TEST_SAVE_PROPERTIES_FILE.getAbsolutePath());
347         ConfigurationAssert.assertConfigurationEquals(conf, checkConfig);
348         return checkConfig;
349     }
350 
351     private void reinitLocationStrategy() {
352         conf.initFileLocator(FileLocatorUtils.fileLocator(conf.getLocator()).locationStrategy(FileLocatorUtils.newDefaultLocationStrategy()).create());
353     }
354 
355     /**
356      * Saves the test configuration to a default output file.
357      *
358      * @throws ConfigurationException if an error occurs
359      */
360     private void saveTestConfig() throws ConfigurationException {
361         final FileHandler handler = new FileHandler(conf);
362         handler.save(TEST_SAVE_PROPERTIES_FILE);
363     }
364 
365     @BeforeEach
366     public void setUp() throws Exception {
367         conf = new PropertiesConfiguration();
368         conf.setListDelimiterHandler(new LegacyListDelimiterHandler(','));
369         load(conf, TEST_PROPERTIES);
370         // remove the test save file if it exists
371         if (TEST_SAVE_PROPERTIES_FILE.exists()) {
372             assertTrue(TEST_SAVE_PROPERTIES_FILE.delete());
373         }
374     }
375 
376     /**
377      * Creates a configuration that can be used for testing copy operations.
378      *
379      * @return the configuration to be copied
380      */
381     private Configuration setUpCopyConfig() {
382         final int count = 25;
383         final Configuration result = new BaseConfiguration();
384         for (int i = 1; i <= count; i++) {
385             result.addProperty("copyKey" + i, "copyValue" + i);
386         }
387         return result;
388     }
389 
390     /**
391      * Tests if properties can be appended by simply calling load() another time.
392      */
393     @Test
394     void testAppend() throws Exception {
395         final File file2 = ConfigurationAssert.getTestFile("threesome.properties");
396         final FileHandler handler = new FileHandler(conf);
397         handler.load(file2);
398         assertEquals("aaa", conf.getString("test.threesome.one"));
399         assertEquals("true", conf.getString("configuration.loaded"));
400     }
401 
402     /**
403      * Tests appending a configuration to the test configuration. Again it has to be ensured that the layout object is
404      * correctly updated.
405      */
406     @Test
407     void testAppendAndSave() throws ConfigurationException {
408         final Configuration copyConf = setUpCopyConfig();
409         conf.append(copyConf);
410         checkCopiedConfig(copyConf);
411     }
412 
413     /**
414      * Tests whether backslashes are correctly handled if lists are parsed. This test is related to CONFIGURATION-418.
415      */
416     @Test
417     void testBackslashEscapingInLists() throws Exception {
418         checkBackslashList("share2");
419         checkBackslashList("share1");
420     }
421 
422     /**
423      * Tests whether another list delimiter character can be set (by using an alternative list delimiter handler).
424      */
425     @Test
426     void testChangingListDelimiter() throws Exception {
427         assertEquals("a^b^c", conf.getString("test.other.delimiter"));
428         final PropertiesConfiguration pc2 = new PropertiesConfiguration();
429         pc2.setListDelimiterHandler(new DefaultListDelimiterHandler('^'));
430         load(pc2, TEST_PROPERTIES);
431         assertEquals("a", pc2.getString("test.other.delimiter"));
432         assertEquals(3, pc2.getList("test.other.delimiter").size());
433     }
434 
435     /**
436      * Tests whether a clear() operation clears the footer comment.
437      */
438     @Test
439     void testClearFooterComment() {
440         conf.clear();
441         assertNull(conf.getFooter());
442         assertNull(conf.getHeader());
443     }
444 
445     /**
446      * Tests whether a properties configuration can be successfully cloned. It is especially checked whether the layout
447      * object is taken into account.
448      */
449     @Test
450     void testClone() throws ConfigurationException {
451         final PropertiesConfiguration copy = (PropertiesConfiguration) conf.clone();
452         assertNotSame(conf.getLayout(), copy.getLayout());
453         assertEquals(1, conf.getEventListeners(ConfigurationEvent.ANY).size());
454         assertEquals(1, copy.getEventListeners(ConfigurationEvent.ANY).size());
455         assertSame(conf.getLayout(), conf.getEventListeners(ConfigurationEvent.ANY).iterator().next());
456         assertSame(copy.getLayout(), copy.getEventListeners(ConfigurationEvent.ANY).iterator().next());
457         final StringWriter outConf = new StringWriter();
458         new FileHandler(conf).save(outConf);
459         final StringWriter outCopy = new StringWriter();
460         new FileHandler(copy).save(outCopy);
461         assertEquals(outConf.toString(), outCopy.toString());
462     }
463 
464     /**
465      * Tests the clone() method when no layout object exists yet.
466      */
467     @Test
468     void testCloneNullLayout() {
469         conf = new PropertiesConfiguration();
470         final PropertiesConfiguration copy = (PropertiesConfiguration) conf.clone();
471         assertNotSame(conf.getLayout(), copy.getLayout());
472     }
473 
474     /**
475      * Test if the lines starting with # or ! are properly ignored.
476      */
477     @Test
478     void testComment() {
479         assertFalse(conf.containsKey("#comment"));
480         assertFalse(conf.containsKey("!comment"));
481     }
482 
483     private Collection<?> testCompress840(final Iterable<?> object) {
484         final PropertiesConfiguration configuration = new PropertiesConfiguration();
485         final ListDelimiterHandler listDelimiterHandler = configuration.getListDelimiterHandler();
486         listDelimiterHandler.flatten(object, 0);
487         // Stack overflow:
488         listDelimiterHandler.flatten(object, 1);
489         listDelimiterHandler.flatten(object, Integer.MAX_VALUE);
490         listDelimiterHandler.parse(object);
491         configuration.addProperty("foo", object);
492         configuration.toString();
493         return listDelimiterHandler.flatten(object, Integer.MAX_VALUE);
494     }
495 
496     @ParameterizedTest
497     @ValueSource(ints = { 0, 2, 4, 8, 16 })
498     void testCompress840ArrayList(final int size) {
499         final ArrayList<Object> object = new ArrayList<>();
500         for (int i = 0; i < size; i++) {
501             object.add(i);
502         }
503         final Collection<?> result = testCompress840(object);
504         assertNotNull(result);
505         assertEquals(size, result.size());
506         assertEquals(object, result);
507     }
508 
509     @ParameterizedTest
510     @ValueSource(ints = { 0, 2, 4, 8, 16 })
511     void testCompress840ArrayListCycle(final int size) {
512         final ArrayList<Object> object = new ArrayList<>();
513         for (int i = 0; i < size; i++) {
514             object.add(i);
515             object.add(object);
516             object.add(new ArrayList<>(object));
517         }
518         final Collection<?> result = testCompress840(object);
519         assertNotNull(result);
520         assertEquals(size, result.size());
521         object.add(object);
522         testCompress840(object);
523     }
524 
525     @Test
526     void testCompress840BeanContextServicesSupport() {
527         testCompress840(new BeanContextServicesSupport());
528         testCompress840(new BeanContextServicesSupport(new BeanContextServicesSupport()));
529         final BeanContextSupport bcs = new BeanContextSupport();
530         final BeanContextServicesSupport bcss = new BeanContextServicesSupport();
531         bcs.add(FileSystems.getDefault().getPath("bar"));
532         bcss.add(bcs);
533         testCompress840(bcss);
534         bcss.add(FileSystems.getDefault().getPath("bar"));
535         testCompress840(bcss);
536         bcss.add(bcss);
537         testCompress840(bcss);
538     }
539 
540     @Test
541     void testCompress840BeanContextSupport() {
542         testCompress840(new BeanContextSupport());
543         testCompress840(new BeanContextSupport(new BeanContextSupport()));
544         final BeanContextSupport bcs = new BeanContextSupport();
545         bcs.add(FileSystems.getDefault().getPath("bar"));
546         testCompress840(bcs);
547         bcs.add(bcs);
548         testCompress840(bcs);
549     }
550 
551     @ParameterizedTest
552     @ValueSource(ints = { 0, 2, 4, 8, 16 })
553     void testCompress840Exception(final int size) {
554         final ArrayList<Object> object = new ArrayList<>();
555         final Exception bottom = new Exception();
556         object.add(bottom);
557         Exception top = bottom;
558         for (int i = 0; i < size; i++) {
559             object.add(i);
560             top = new Exception(top);
561             object.add(top);
562         }
563         if (bottom != top) {
564             // direct self-causation is not allowed.
565             bottom.initCause(top);
566         }
567         final Collection<?> result = testCompress840(object);
568         assertNotNull(result);
569         assertEquals(size * 2 + 1, result.size());
570         assertEquals(object, result);
571     }
572 
573     @ParameterizedTest
574     @ValueSource(ints = { 0, 2, 4, 8, 16 })
575     void testCompress840Path(final int size) {
576         final PriorityQueue<Object> object = new PriorityQueue<>();
577         for (int i = 0; i < size; i++) {
578             object.add(FileSystems.getDefault().getPath("foo"));
579             object.add(FileSystems.getDefault().getPath("foo", "bar"));
580         }
581         testCompress840(object);
582     }
583 
584     @ParameterizedTest
585     @ValueSource(ints = { 0, 2, 4, 8, 16 })
586     void testCompress840PriorityQueue(final int size) {
587         final PriorityQueue<Object> object = new PriorityQueue<>();
588         for (int i = 0; i < size; i++) {
589             object.add(FileSystems.getDefault().getPath("foo"));
590         }
591         testCompress840(object);
592     }
593 
594     @Test
595     void testConfiguration() throws ConfigurationException {
596         final Configurations configManager = new Configurations();
597         final Configuration config = configManager.properties("src/test/resources/config/test.properties");
598 
599         assertTrue(config.containsValue("jndivalue2"));
600         assertFalse(config.containsValue("notFound"));
601         assertFalse(config.containsValue(null));
602         assertFalse(config.containsValue(""));
603     }
604 
605     /**
606      * Tests copying another configuration into the test configuration. This test ensures that the layout object is informed
607      * about the newly added properties.
608      */
609     @Test
610     void testCopyAndSave() throws ConfigurationException {
611         final Configuration copyConf = setUpCopyConfig();
612         conf.copy(copyConf);
613         checkCopiedConfig(copyConf);
614     }
615 
616     /**
617      * Tests whether include files can be disabled.
618      */
619     @Test
620     void testDisableIncludes() throws ConfigurationException, IOException {
621         final String content = PropertiesConfiguration.getInclude() + " = nonExistingIncludeFile" + CR + PROP_NAME + " = " + PROP_VALUE + CR;
622         final StringReader in = new StringReader(content);
623         conf = new PropertiesConfiguration();
624         conf.setIncludesAllowed(false);
625         conf.read(in);
626         assertEquals(PROP_VALUE, conf.getString(PROP_NAME));
627     }
628 
629     @Test
630     void testDisableListDelimiter() throws Exception {
631         assertEquals(4, conf.getList("test.mixed.array").size());
632 
633         final PropertiesConfiguration pc2 = new PropertiesConfiguration();
634         load(pc2, TEST_PROPERTIES);
635         assertEquals(2, pc2.getList("test.mixed.array").size());
636     }
637 
638     /**
639      * Tests that empty properties are treated as the empty string (rather than as null).
640      */
641     @Test
642     void testEmpty() {
643         checkEmpty("test.empty");
644     }
645 
646     /**
647      * Tests that properties are detected that do not have a separator and a value.
648      */
649     @Test
650     void testEmptyNoSeparator() {
651         checkEmpty("test.empty2");
652     }
653 
654     @Test
655     void testEscapedKey() throws Exception {
656         conf.clear();
657         final FileHandler handler = new FileHandler(conf);
658         handler.load(new StringReader("\\u0066\\u006f\\u006f=bar"));
659 
660         assertEquals("bar", conf.getString("foo"));
661     }
662 
663     /**
664      * Check that key/value separators can be part of a key.
665      */
666     @Test
667     void testEscapedKeyValueSeparator() {
668         assertEquals("foo", conf.getProperty("test.separator=in.key"));
669         assertEquals("bar", conf.getProperty("test.separator:in.key"));
670         assertEquals("foo", conf.getProperty("test.separator\tin.key"));
671         assertEquals("bar", conf.getProperty("test.separator\fin.key"));
672         assertEquals("foo", conf.getProperty("test.separator in.key"));
673     }
674 
675     /**
676      * Tests the escaping of quotation marks in a properties value. This test is related to CONFIGURATION-516.
677      */
678     @Test
679     void testEscapeQuote() throws ConfigurationException {
680         conf.clear();
681         final String text = "\"Hello World!\"";
682         conf.setProperty(PROP_NAME, text);
683         final StringWriter out = new StringWriter();
684         new FileHandler(conf).save(out);
685         assertTrue(out.toString().contains(text));
686         saveTestConfig();
687         final PropertiesConfiguration c2 = new PropertiesConfiguration();
688         load(c2, TEST_SAVE_PROPERTIES_FILE.getAbsolutePath());
689         assertEquals(text, c2.getString(PROP_NAME));
690     }
691 
692     /**
693      * Test the creation of a file containing a '#' in its name.
694      */
695     @Test
696     void testFileWithSharpSymbol() throws Exception {
697         final File file = newFile("sharp#1.properties", tempFolder);
698 
699         final PropertiesConfiguration conf = new PropertiesConfiguration();
700         final FileHandler handler = new FileHandler(conf);
701         handler.setFile(file);
702         handler.load();
703         handler.save();
704 
705         assertTrue(file.exists());
706     }
707 
708     /**
709      * Tests whether read access to the footer comment is synchronized.
710      */
711     @Test
712     void testGetFooterSynchronized() {
713         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
714         conf.setSynchronizer(sync);
715         assertNotNull(conf.getFooter());
716         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
717     }
718 
719     /**
720      * Tests whether read access to the header comment is synchronized.
721      */
722     @Test
723     void testGetHeaderSynchronized() {
724         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
725         conf.setSynchronizer(sync);
726         assertNull(conf.getHeader());
727         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
728     }
729 
730     /**
731      * Tests whether a default IOFactory is set.
732      */
733     @Test
734     void testGetIOFactoryDefault() {
735         assertNotNull(conf.getIOFactory());
736     }
737 
738     /**
739      * Tests accessing the layout object.
740      */
741     @Test
742     void testGetLayout() {
743         final PropertiesConfigurationLayout layout = conf.getLayout();
744         assertNotNull(layout);
745         assertSame(layout, conf.getLayout());
746         conf.setLayout(null);
747         final PropertiesConfigurationLayout layout2 = conf.getLayout();
748         assertNotNull(layout2);
749         assertNotSame(layout, layout2);
750     }
751 
752     @Test
753     void testGetStringWithEscapedChars() {
754         final String property = conf.getString("test.unescape");
755         assertEquals("This \n string \t contains \" escaped \\ characters", property);
756     }
757 
758     @Test
759     void testGetStringWithEscapedComma() {
760         final String property = conf.getString("test.unescape.list-separator");
761         assertEquals("This string contains , an escaped list separator", property);
762     }
763 
764     @Test
765     void testIncludeIncludeLoadAllOnNotFound() throws Exception {
766         final PropertiesConfiguration pc = new PropertiesConfiguration();
767         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
768         final FileHandler handler = new FileHandler(pc);
769         handler.setBasePath(TEST_BASE_PATH);
770         handler.setFileName("include-include-not-found.properties");
771         handler.load();
772         assertEquals("valueA", pc.getString("keyA"));
773         assertEquals("valueB", pc.getString("keyB"));
774     }
775 
776     @Test
777     void testIncludeIncludeLoadCyclicalReferenceFail() throws Exception {
778         final PropertiesConfiguration pc = new PropertiesConfiguration();
779         final FileHandler handler = new FileHandler(pc);
780         handler.setBasePath(TEST_BASE_PATH);
781         handler.setFileName("include-include-cyclical-reference.properties");
782         assertThrows(ConfigurationException.class, handler::load);
783         assertNull(pc.getString("keyA"));
784     }
785 
786     @Test
787     void testIncludeIncludeLoadCyclicalReferenceIgnore() throws Exception {
788         final PropertiesConfiguration pc = new PropertiesConfiguration();
789         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
790         final FileHandler handler = new FileHandler(pc);
791         handler.setBasePath(TEST_BASE_PATH);
792         handler.setFileName("include-include-cyclical-reference.properties");
793         handler.load();
794         assertEquals("valueA", pc.getString("keyA"));
795     }
796 
797     /**
798      * Tests including properties when they are loaded from a nested directory structure.
799      */
800     @Test
801     void testIncludeInSubDir() throws ConfigurationException {
802         final CombinedConfigurationBuilder builder = new CombinedConfigurationBuilder();
803         builder.configure(new FileBasedBuilderParametersImpl().setFileName("testFactoryPropertiesInclude.xml"));
804         final Configuration config = builder.getConfiguration();
805         assertTrue(config.getBoolean("deeptest"));
806         assertTrue(config.getBoolean("deepinclude"));
807         assertFalse(config.containsKey("deeptestinvalid"));
808     }
809 
810     @Test
811     void testIncludeLoadAllOnLoadBadHostException() throws Exception {
812         final PropertiesConfiguration pc = new PropertiesConfiguration();
813         final ExceptionList list = new ExceptionList();
814         pc.setIncludeListener(list);
815         final FileHandler handler = new FileHandler(pc);
816         // @formatter:off
817         final CombinedLocationStrategy.Builder builder = new CombinedLocationStrategy.Builder()
818                 .setSchemes(new TreeSet<>(Arrays.asList(ALL_SCHEMES.split(","))))
819                 .setHostsRegEx(new TreeSet<>(Arrays.asList("GrantThisHost")));
820         handler.setLocationStrategy(builder.setSubStrategies(Arrays.asList(
821                 new ProvidedURLLocationStrategy(builder),
822                 new FileSystemLocationStrategy(builder),
823                 new AbsoluteNameLocationStrategy(builder),
824                 new BasePathLocationStrategy(builder),
825                 new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(true).getUnchecked(),
826                 new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(false).getUnchecked(),
827                 new ClasspathLocationStrategy(builder)))
828                 .get());
829         // @formatter:on
830         handler.setBasePath(TEST_BASE_PATH);
831         handler.setFileName("include-load-url-host-unknown-exception.properties");
832         handler.load();
833         assertFalse(list.exceptions.isEmpty());
834         assertInstanceOf(ConfigurationDeniedException.class, list.exceptions.get(0).getCause());
835         assertEquals("valueA", pc.getString("keyA"));
836     }
837 
838     @Test
839     @SetSystemProperty(key = KEY_SCHEMES, value = ALL_SCHEMES)
840     void testIncludeLoadAllOnLoadBadUrlException() throws Exception {
841         final PropertiesConfiguration pc = new PropertiesConfiguration();
842         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
843         final FileHandler handler = new FileHandler(pc);
844         // pick up @SetSystemProperty
845         handler.setLocationStrategy(FileLocatorUtils.newDefaultLocationStrategy());
846         handler.setBasePath(TEST_BASE_PATH);
847         handler.setFileName("include-load-url-host-unknown-exception.properties");
848         handler.load();
849         assertEquals("valueA", pc.getString("keyA"));
850     }
851 
852     @Test
853     @SetSystemProperty(key = KEY_SCHEMES, value = ALL_SCHEMES)
854     void testIncludeLoadAllOnLoadBadUrlExceptionManualDefault() throws Exception {
855         final PropertiesConfiguration pc = new PropertiesConfiguration();
856         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
857         final FileHandler handler = new FileHandler(pc);
858         // pick up @SetSystemProperty
859         // @formatter:off
860         handler.setLocationStrategy(new CombinedLocationStrategy(Arrays.asList(
861                 new ProvidedURLLocationStrategy(),
862                 new FileSystemLocationStrategy(),
863                 new AbsoluteNameLocationStrategy(),
864                 new BasePathLocationStrategy(),
865                 new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(true).getUnchecked(),
866                 new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(false).getUnchecked(),
867                 new ClasspathLocationStrategy())));
868         // @formatter:on
869         handler.setBasePath(TEST_BASE_PATH);
870         handler.setFileName("include-load-url-host-unknown-exception.properties");
871         handler.load();
872         assertEquals("valueA", pc.getString("keyA"));
873     }
874 
875     @Test
876     void testIncludeLoadAllOnLoadBadUrlExceptionManualPropagate() throws Exception {
877         final PropertiesConfiguration pc = new PropertiesConfiguration();
878         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
879         final FileHandler handler = new FileHandler(pc);
880         final CombinedLocationStrategy.Builder builder = new CombinedLocationStrategy.Builder()
881                 .setSchemes(new TreeSet<>(Arrays.asList(ALL_SCHEMES.split(","))));
882         // @formatter:off
883         handler.setLocationStrategy(builder.setSubStrategies(Arrays.asList(
884                 new ProvidedURLLocationStrategy(builder),
885                 new FileSystemLocationStrategy(builder),
886                 new AbsoluteNameLocationStrategy(builder),
887                 new BasePathLocationStrategy(builder),
888                 new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(true).getUnchecked(),
889                 new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(false).getUnchecked(),
890                 new ClasspathLocationStrategy(builder)))
891                 .get());
892         // @formatter:on
893         handler.setBasePath(TEST_BASE_PATH);
894         handler.setFileName("include-load-url-host-unknown-exception.properties");
895         handler.load();
896         assertEquals("valueA", pc.getString("keyA"));
897     }
898 
899     @Test
900     void testIncludeLoadAllOnLoadFileException() throws Exception {
901         final PropertiesConfiguration pc = new PropertiesConfiguration();
902         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
903         final FileHandler handler = new FileHandler(pc);
904         handler.setBasePath(TEST_BASE_PATH);
905         handler.setFileName("include-load-url-file-exception.properties");
906         handler.load();
907         assertEquals("valueA", pc.getString("keyA"));
908     }
909 
910     @Test
911     @SetSystemProperty(key = KEY_SCHEMES, value = ALL_SCHEMES)
912     void testIncludeLoadAllOnLoadUrlException() throws Exception {
913         final PropertiesConfiguration pc = new PropertiesConfiguration();
914         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
915         final FileHandler handler = new FileHandler(pc);
916         // pick up @SetSystemProperty
917         handler.setLocationStrategy(FileLocatorUtils.newDefaultLocationStrategy());
918         handler.setBasePath(TEST_BASE_PATH);
919         handler.setFileName("include-load-url-http-exception.properties");
920         handler.load();
921         assertEquals("valueA", pc.getString("keyA"));
922     }
923 
924     @Test
925     void testIncludeLoadAllOnNotFound() throws Exception {
926         final PropertiesConfiguration pc = new PropertiesConfiguration();
927         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
928         final FileHandler handler = new FileHandler(pc);
929         handler.setBasePath(TEST_BASE_PATH);
930         handler.setFileName("include-not-found.properties");
931         handler.load();
932         assertEquals("valueA", pc.getString("keyA"));
933     }
934 
935     @Test
936     void testIncludeLoadCyclicalMultiStepReferenceFail() throws Exception {
937         final PropertiesConfiguration pc = new PropertiesConfiguration();
938         final FileHandler handler = new FileHandler(pc);
939         handler.setBasePath(TEST_BASE_PATH);
940         handler.setFileName("include-cyclical-root.properties");
941         assertThrows(ConfigurationException.class, handler::load);
942         assertNull(pc.getString("keyA"));
943     }
944 
945     @Test
946     void testIncludeLoadCyclicalMultiStepReferenceIgnore() throws Exception {
947         final PropertiesConfiguration pc = new PropertiesConfiguration();
948         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
949         final FileHandler handler = new FileHandler(pc);
950         handler.setBasePath(TEST_BASE_PATH);
951         handler.setFileName("include-cyclical-root.properties");
952         handler.load();
953         assertEquals("valueA", pc.getString("keyA"));
954     }
955 
956     @Test
957     void testIncludeLoadCyclicalReferenceFail() throws Exception {
958         final PropertiesConfiguration pc = new PropertiesConfiguration();
959         final FileHandler handler = new FileHandler(pc);
960         handler.setBasePath(TEST_BASE_PATH);
961         handler.setFileName("include-cyclical-reference.properties");
962         assertThrows(ConfigurationException.class, handler::load);
963         assertNull(pc.getString("keyA"));
964     }
965 
966     @Test
967     void testIncludeLoadCyclicalReferenceIgnore() throws Exception {
968         final PropertiesConfiguration pc = new PropertiesConfiguration();
969         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
970         final FileHandler handler = new FileHandler(pc);
971         handler.setBasePath(TEST_BASE_PATH);
972         handler.setFileName("include-cyclical-reference.properties");
973         handler.load();
974         assertEquals("valueA", pc.getString("keyA"));
975     }
976 
977     @Test
978     void testIncludeLoadUrlBadScheme() throws Exception {
979         final PropertiesConfiguration pc = new PropertiesConfiguration();
980         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
981         final FileHandler handler = new FileHandler(pc);
982         handler.setBasePath(TEST_BASE_PATH);
983         handler.setFileName("include-load-url-bad-scheme-exception.properties");
984         handler.load();
985         assertEquals("valueA", pc.getString("keyA"));
986     }
987 
988     /**
989      * Tests initializing a properties configuration from a non existing file. There was a bug, which caused properties
990      * getting lost when later save() is called.
991      */
992     @Test
993     void testInitFromNonExistingFile() throws ConfigurationException {
994         final String testProperty = "test.successfull";
995         conf = new PropertiesConfiguration();
996         final FileHandler handler = new FileHandler(conf);
997         handler.setFile(TEST_SAVE_PROPERTIES_FILE);
998         conf.addProperty(testProperty, "true");
999         handler.save();
1000         checkSavedConfig();
1001     }
1002 
1003     @Test
1004     void testInMemoryCreatedSave() throws Exception {
1005         conf = new PropertiesConfiguration();
1006         // add an array of strings to the configuration
1007         conf.addProperty("string", "value1");
1008         final List<Object> list = new ArrayList<>();
1009         for (int i = 1; i < 5; i++) {
1010             list.add("value" + i);
1011         }
1012         conf.addProperty("array", list);
1013 
1014         // save the configuration
1015         saveTestConfig();
1016         assertTrue(TEST_SAVE_PROPERTIES_FILE.exists());
1017 
1018         // read the configuration and compare the properties
1019         checkSavedConfig();
1020     }
1021 
1022     /**
1023      * Tests whether comment lines are correctly detected.
1024      */
1025     @Test
1026     void testIsCommentLine() {
1027         assertTrue(PropertiesConfiguration.isCommentLine("# a comment"));
1028         assertTrue(PropertiesConfiguration.isCommentLine("! a comment"));
1029         assertTrue(PropertiesConfiguration.isCommentLine("#a comment"));
1030         assertTrue(PropertiesConfiguration.isCommentLine("    ! a comment"));
1031         assertFalse(PropertiesConfiguration.isCommentLine("   a#comment"));
1032     }
1033 
1034     /**
1035      * Tests that {@link PropertiesConfiguration.JupIOFactory} reads the same keys and values as {@link Properties} based on
1036      * a test file.
1037      */
1038     @Test
1039     void testJupRead() throws IOException, ConfigurationException {
1040         conf.clear();
1041         conf.setIOFactory(new PropertiesConfiguration.JupIOFactory());
1042 
1043         final String testFilePath = ConfigurationAssert.getTestFile("jup-test.properties").getAbsolutePath();
1044 
1045         load(conf, testFilePath);
1046 
1047         final Properties jup = new Properties();
1048         try (InputStream in = Files.newInputStream(Paths.get(testFilePath))) {
1049             jup.load(in);
1050         }
1051 
1052         @SuppressWarnings("unchecked")
1053         final Set<Object> pcKeys = new HashSet<>(IteratorUtils.toList(conf.getKeys()));
1054         assertEquals(jup.keySet(), pcKeys);
1055 
1056         for (final Object key : jup.keySet()) {
1057             final String keyString = key.toString();
1058             assertEquals(jup.getProperty(keyString), conf.getProperty(keyString), "Wrong property value for '" + keyString + "'");
1059         }
1060     }
1061 
1062     /**
1063      * Tests that {@link PropertiesConfiguration.JupIOFactory} writes properties in a way that allows {@link Properties} to
1064      * read them exactly like they were set.
1065      */
1066     @Test
1067     void testJupWrite() throws IOException, ConfigurationException {
1068         conf.clear();
1069         conf.setIOFactory(new PropertiesConfiguration.JupIOFactory());
1070 
1071         final String testFilePath = ConfigurationAssert.getTestFile("jup-test.properties").getAbsolutePath();
1072 
1073         // read the test properties and set them on the PropertiesConfiguration
1074         final Properties origProps = new Properties();
1075         try (InputStream in = Files.newInputStream(Paths.get(testFilePath))) {
1076             origProps.load(in);
1077         }
1078         for (final Object key : origProps.keySet()) {
1079             final String keyString = key.toString();
1080             conf.setProperty(keyString, origProps.getProperty(keyString));
1081         }
1082 
1083         // save the configuration
1084         saveTestConfig();
1085         assertTrue(TEST_SAVE_PROPERTIES_FILE.exists());
1086 
1087         // load the saved file...
1088         final Properties testProps = new Properties();
1089         try (InputStream in = Files.newInputStream(TEST_SAVE_PROPERTIES_FILE.toPath())) {
1090             testProps.load(in);
1091         }
1092 
1093         // ... and compare the properties to the originals
1094         @SuppressWarnings("unchecked")
1095         final Set<Object> pcKeys = new HashSet<>(IteratorUtils.toList(conf.getKeys()));
1096         assertEquals(testProps.keySet(), pcKeys);
1097 
1098         for (final Object key : testProps.keySet()) {
1099             final String keyString = key.toString();
1100             assertEquals(testProps.getProperty(keyString), conf.getProperty(keyString), "Wrong property value for '" + keyString + "'");
1101         }
1102     }
1103 
1104     /**
1105      * Tests that {@link PropertiesConfiguration.JupIOFactory} writes properties in a way that allows {@link Properties} to
1106      * read them exactly like they were set. This test writes in UTF-8 encoding, with Unicode escapes turned off.
1107      */
1108     @Test
1109     void testJupWriteUtf8WithoutUnicodeEscapes() throws IOException, ConfigurationException {
1110         conf.clear();
1111         conf.setIOFactory(new PropertiesConfiguration.JupIOFactory(false));
1112 
1113         final String testFilePath = ConfigurationAssert.getTestFile("jup-test.properties").getAbsolutePath();
1114 
1115         // read the test properties and set them on the PropertiesConfiguration
1116         final Properties origProps = new Properties();
1117         try (InputStream in = Files.newInputStream(Paths.get(testFilePath))) {
1118             origProps.load(in);
1119         }
1120         for (final Object key : origProps.keySet()) {
1121             final String keyString = key.toString();
1122             conf.setProperty(keyString, origProps.getProperty(keyString));
1123         }
1124 
1125         // save the configuration as UTF-8
1126         final FileHandler handler = new FileHandler(conf);
1127         handler.setEncoding(StandardCharsets.UTF_8.name());
1128         handler.save(TEST_SAVE_PROPERTIES_FILE);
1129         assertTrue(TEST_SAVE_PROPERTIES_FILE.exists());
1130 
1131         // load the saved file...
1132         final Properties testProps = new Properties();
1133         try (BufferedReader in = Files.newBufferedReader(TEST_SAVE_PROPERTIES_FILE.toPath(), StandardCharsets.UTF_8)) {
1134             testProps.load(in);
1135         }
1136 
1137         // ... and compare the properties to the originals
1138         @SuppressWarnings("unchecked")
1139         final Set<Object> pcKeys = new HashSet<>(IteratorUtils.toList(conf.getKeys()));
1140         assertEquals(testProps.keySet(), pcKeys);
1141 
1142         for (final Object key : testProps.keySet()) {
1143             final String keyString = key.toString();
1144             assertEquals(testProps.getProperty(keyString), conf.getProperty(keyString), "Wrong property value for '" + keyString + "'");
1145         }
1146 
1147         // ensure that the written properties file contains no Unicode escapes
1148         for (final String line : Files.readAllLines(TEST_SAVE_PROPERTIES_FILE.toPath())) {
1149             assertFalse(line.contains("\\u"));
1150         }
1151     }
1152 
1153     /**
1154      * Tests that the property separators are retained when saving the configuration.
1155      */
1156     @Test
1157     void testKeepSeparators() throws ConfigurationException, IOException {
1158         saveTestConfig();
1159         // @formatter:off
1160         final Set<String> separatorTests = new HashSet<>(Arrays.asList(
1161                 "test.separator.equal = foo",
1162                 "test.separator.colon : foo",
1163                 "test.separator.tab\tfoo",
1164                 "test.separator.whitespace foo",
1165                 "test.separator.no.space=foo"));
1166         // @formatter:on
1167         final Set<String> foundLines = new HashSet<>();
1168         try (BufferedReader in = new BufferedReader(new FileReader(TEST_SAVE_PROPERTIES_FILE))) {
1169             String s;
1170             while ((s = in.readLine()) != null) {
1171                 for (final String separatorTest : separatorTests) {
1172                     if (separatorTest.equals(s)) {
1173                         foundLines.add(s);
1174                     }
1175                 }
1176             }
1177         }
1178         assertEquals(separatorTests, foundLines);
1179     }
1180 
1181     /**
1182      * Test all acceptable key/value separators ('=', ':' or white spaces).
1183      */
1184     @Test
1185     void testKeyValueSeparators() {
1186         assertEquals("foo", conf.getProperty("test.separator.equal"));
1187         assertEquals("foo", conf.getProperty("test.separator.colon"));
1188         assertEquals("foo", conf.getProperty("test.separator.tab"));
1189         assertEquals("foo", conf.getProperty("test.separator.formfeed"));
1190         assertEquals("foo", conf.getProperty("test.separator.whitespace"));
1191     }
1192 
1193     @Test
1194     void testLargeKey() throws Exception {
1195         conf.clear();
1196         final String key = String.join("", Collections.nCopies(10000, "x"));
1197         final FileHandler handler = new FileHandler(conf);
1198         handler.load(new StringReader(key));
1199 
1200         assertEquals("", conf.getString(key));
1201     }
1202 
1203     /**
1204      * Tests whether the correct line separator is used.
1205      */
1206     @Test
1207     void testLineSeparator() throws ConfigurationException {
1208         final String eol = System.lineSeparator();
1209         conf = new PropertiesConfiguration();
1210         conf.setHeader("My header");
1211         conf.setProperty("prop", "value");
1212 
1213         final StringWriter out = new StringWriter();
1214         new FileHandler(conf).save(out);
1215         final String content = out.toString();
1216         assertEquals(0, content.indexOf("# My header" + eol + eol));
1217         assertTrue(content.contains("prop = value" + eol));
1218     }
1219 
1220     /**
1221      * Tests {@code List} parsing.
1222      */
1223     @Test
1224     void testList() throws Exception {
1225         final List<Object> packages = conf.getList("packages");
1226         // we should get 3 packages here
1227         assertEquals(3, packages.size());
1228     }
1229 
1230     @Test
1231     void testLoad() throws Exception {
1232         final String loaded = conf.getString("configuration.loaded");
1233         assertEquals("true", loaded);
1234     }
1235 
1236     @Test
1237     void testLoadFromFile() throws Exception {
1238         final File file = ConfigurationAssert.getTestFile("test.properties");
1239         conf.clear();
1240         final FileHandler handler = new FileHandler(conf);
1241         handler.setFile(file);
1242         handler.load();
1243 
1244         assertEquals("true", conf.getString("configuration.loaded"));
1245     }
1246 
1247     /**
1248      * test if includes properties get loaded too
1249      */
1250     @Test
1251     void testLoadInclude() throws Exception {
1252         final String loaded = conf.getString("include.loaded");
1253         assertEquals("true", loaded);
1254     }
1255 
1256     /**
1257      * Tests whether the correct file system is used when loading an include file. This test is related to
1258      * CONFIGURATION-609.
1259      */
1260     @Test
1261     void testLoadIncludeFileViaFileSystem() throws ConfigurationException {
1262         conf.clear();
1263         conf.addProperty("include", "include.properties");
1264         saveTestConfig();
1265 
1266         final FileSystem fs = new DefaultFileSystem() {
1267             @Override
1268             public InputStream getInputStream(final URL url) throws ConfigurationException {
1269                 if (url.toString().endsWith("include.properties")) {
1270                     return new ByteArrayInputStream("test.outcome = success".getBytes(StandardCharsets.UTF_8));
1271                 }
1272                 return super.getInputStream(url);
1273             }
1274         };
1275         final Parameters params = new Parameters();
1276         final FileBasedConfigurationBuilder<PropertiesConfiguration> builder = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class);
1277         builder.configure(params.fileBased().setFile(TEST_SAVE_PROPERTIES_FILE).setBasePath(ConfigurationAssert.OUT_DIR.toURI().toString()).setFileSystem(fs));
1278         final PropertiesConfiguration configuration = builder.getConfiguration();
1279         assertEquals("success", configuration.getString("test.outcome"));
1280     }
1281 
1282     /**
1283      * Tests if included files are loaded when the source lies in the class path.
1284      */
1285     @Test
1286     void testLoadIncludeFromClassPath() {
1287         assertEquals("true", conf.getString("include.loaded"));
1288     }
1289 
1290     /**
1291      * Tests whether include files can be resolved if a configuration file is read from a reader.
1292      */
1293     @Test
1294     void testLoadIncludeFromReader() throws ConfigurationException {
1295         final StringReader in = new StringReader(PropertiesConfiguration.getInclude() + " = " + ConfigurationAssert.getTestURL("include.properties"));
1296         conf = new PropertiesConfiguration();
1297         final FileHandler handler = new FileHandler(conf);
1298         handler.load(in);
1299         assertEquals("true", conf.getString("include.loaded"));
1300     }
1301 
1302     /**
1303      * test if includes properties from interpolated file name get loaded
1304      */
1305     @Test
1306     void testLoadIncludeInterpol() throws Exception {
1307         final String loaded = conf.getString("include.interpol.loaded");
1308         assertEquals("true", loaded);
1309     }
1310 
1311     @Test
1312     void testLoadIncludeOptional() throws Exception {
1313         final PropertiesConfiguration pc = new PropertiesConfiguration();
1314         final FileHandler handler = new FileHandler(pc);
1315         handler.setBasePath(TEST_BASE_PATH);
1316         handler.setFileName("includeoptional.properties");
1317         handler.load();
1318 
1319         assertTrue(pc.getBoolean("includeoptional.loaded"));
1320     }
1321 
1322     @Test
1323     void testLoadUnexistingFile() {
1324         assertThrows(ConfigurationException.class, () -> load(conf, "unexisting file"));
1325     }
1326 
1327     @Test
1328     void testLoadViaPropertyWithBasePath() throws Exception {
1329         final PropertiesConfiguration pc = new PropertiesConfiguration();
1330         final FileHandler handler = new FileHandler(pc);
1331         handler.setBasePath(TEST_BASE_PATH);
1332         handler.setFileName("test.properties");
1333         handler.load();
1334 
1335         assertTrue(pc.getBoolean("test.boolean"));
1336     }
1337 
1338     @Test
1339     void testLoadViaPropertyWithBasePath2() throws Exception {
1340         final PropertiesConfiguration pc = new PropertiesConfiguration();
1341         final FileHandler handler = new FileHandler(pc);
1342         handler.setBasePath(TEST_BASE_PATH_2);
1343         handler.setFileName("test.properties");
1344         handler.load();
1345 
1346         assertTrue(pc.getBoolean("test.boolean"));
1347     }
1348 
1349     @Test
1350     void testMixedArray() {
1351         final String[] array = conf.getStringArray("test.mixed.array");
1352 
1353         assertArrayEquals(new String[] {"a", "b", "c", "d"}, array);
1354     }
1355 
1356     @Test
1357     void testMultilines() {
1358         final String property = "This is a value spread out across several adjacent natural lines by escaping the line terminator with "
1359             + "a backslash character.";
1360 
1361         assertEquals(property, conf.getString("test.multilines"));
1362     }
1363 
1364     /**
1365      * Tests whether multiple include files can be resolved.
1366      */
1367     @Test
1368     void testMultipleIncludeFiles() throws ConfigurationException {
1369         conf = new PropertiesConfiguration();
1370         final FileHandler handler = new FileHandler(conf);
1371         handler.load(ConfigurationAssert.getTestFile("config/testMultiInclude.properties"));
1372         assertEquals("topValue", conf.getString("top"));
1373         assertEquals(100, conf.getInt("property.c"));
1374         assertTrue(conf.getBoolean("include.loaded"));
1375     }
1376 
1377     /**
1378      * Tests escaping of an end of line with a backslash.
1379      */
1380     @Test
1381     void testNewLineEscaping() {
1382         final List<Object> list = conf.getList("test.path");
1383         assertEquals(Arrays.asList("C:\\path1\\", "C:\\path2\\", "C:\\path3\\complex\\test\\"), list);
1384     }
1385 
1386     /**
1387      * Tests the propertyLoaded() method for a simple property.
1388      */
1389     @Test
1390     void testPropertyLoaded() throws ConfigurationException {
1391         final DummyLayout layout = new DummyLayout();
1392         conf.setLayout(layout);
1393         conf.propertyLoaded("layoutLoadedProperty", "yes", null);
1394         assertEquals(0, layout.loadCalls);
1395         assertEquals("yes", conf.getString("layoutLoadedProperty"));
1396     }
1397 
1398     /**
1399      * Tests the propertyLoaded() method for an include property.
1400      */
1401     @Test
1402     void testPropertyLoadedInclude() throws ConfigurationException {
1403         final DummyLayout layout = new DummyLayout();
1404         conf.setLayout(layout);
1405         conf.propertyLoaded(PropertiesConfiguration.getInclude(), "testClasspath.properties,testEqual.properties", new ArrayDeque<>());
1406         assertEquals(2, layout.loadCalls);
1407         assertFalse(conf.containsKey(PropertiesConfiguration.getInclude()));
1408     }
1409 
1410     /**
1411      * Tests propertyLoaded() for an include property, when includes are disabled.
1412      */
1413     @Test
1414     void testPropertyLoadedIncludeNotAllowed() throws ConfigurationException {
1415         final DummyLayout layout = new DummyLayout();
1416         conf.setLayout(layout);
1417         conf.setIncludesAllowed(false);
1418         conf.propertyLoaded(PropertiesConfiguration.getInclude(), "testClassPath.properties,testEqual.properties", null);
1419         assertEquals(0, layout.loadCalls);
1420         assertFalse(conf.containsKey(PropertiesConfiguration.getInclude()));
1421     }
1422 
1423     /**
1424      * Tests a direct invocation of the read() method. This is not allowed because certain initializations have not been
1425      * done. This test is related to CONFIGURATION-641.
1426      */
1427     @Test
1428     void testReadCalledDirectly() throws IOException {
1429         conf = new PropertiesConfiguration();
1430         try (Reader in = new FileReader(ConfigurationAssert.getTestFile("test.properties"))) {
1431             final ConfigurationException e = assertThrows(ConfigurationException.class, () -> conf.read(in));
1432             assertTrue(e.getMessage().contains("FileHandler"));
1433         }
1434     }
1435 
1436     /**
1437      * Tests whether a footer comment is correctly read.
1438      */
1439     @Test
1440     void testReadFooterComment() {
1441         assertEquals("\n# This is a foot comment\n", conf.getFooter());
1442         assertEquals("\nThis is a foot comment\n", conf.getLayout().getCanonicalFooterCooment(false));
1443     }
1444 
1445     /**
1446      * Tests that references to other properties work
1447      */
1448     @Test
1449     void testReference() throws Exception {
1450         assertEquals("baseextra", conf.getString("base.reference"));
1451     }
1452 
1453     @Test
1454     void testSave() throws Exception {
1455         // add an array of strings to the configuration
1456         conf.addProperty("string", "value1");
1457         final List<Object> list = new ArrayList<>();
1458         for (int i = 1; i < 5; i++) {
1459             list.add("value" + i);
1460         }
1461         conf.addProperty("array", list);
1462 
1463         // save the configuration
1464         saveTestConfig();
1465         assertTrue(TEST_SAVE_PROPERTIES_FILE.exists());
1466 
1467         // read the configuration and compare the properties
1468         checkSavedConfig();
1469     }
1470 
1471     /**
1472      * Tests whether the escape character for list delimiters can be itself escaped and survives a save operation.
1473      */
1474     @Test
1475     void testSaveEscapedEscapingCharacter() throws ConfigurationException {
1476         conf.addProperty("test.dirs", "C:\\Temp\\\\,D:\\Data\\\\,E:\\Test\\");
1477         final List<Object> dirs = conf.getList("test.dirs");
1478         assertEquals(3, dirs.size());
1479         saveTestConfig();
1480         checkSavedConfig();
1481     }
1482 
1483     @Test
1484     void testSaveMissingFileName() {
1485         final FileHandler handler = new FileHandler(conf);
1486         assertThrows(ConfigurationException.class, handler::save);
1487     }
1488 
1489     @Test
1490     void testSaveToCustomURL() throws Exception {
1491         // save the configuration to a custom URL
1492         final URL url = new URL("foo", "", 0, newFile("testsave-custom-url.properties", tempFolder).getAbsolutePath(), new FileURLStreamHandler());
1493         final FileHandler handlerSave = new FileHandler(conf);
1494         handlerSave.save(url);
1495 
1496         // reload the configuration
1497         final PropertiesConfiguration config2 = new PropertiesConfiguration();
1498         final FileHandler handlerLoad = new FileHandler(config2);
1499         handlerLoad.load(url);
1500         assertEquals("true", config2.getString("configuration.loaded"));
1501     }
1502 
1503     /**
1504      * Tests saving a file-based configuration to a HTTP server when the server reports a failure. This should cause an
1505      * exception.
1506      */
1507     @Test
1508     void testSaveToHTTPServerFail() throws Exception {
1509         final MockHttpURLStreamHandler handler = new MockHttpURLStreamHandler(HttpURLConnection.HTTP_BAD_REQUEST, TEST_SAVE_PROPERTIES_FILE);
1510         final URL url = new URL(null, "http://jakarta.apache.org", handler);
1511         final FileHandler fileHandler = new FileHandler(conf);
1512         final ConfigurationException cex = assertThrows(ConfigurationException.class, () -> fileHandler.save(url));
1513         assertInstanceOf(IOException.class, cex.getCause());
1514     }
1515 
1516     /**
1517      * Tests saving a file-based configuration to a HTTP server.
1518      */
1519     @Test
1520     void testSaveToHTTPServerSuccess() throws Exception {
1521         final MockHttpURLStreamHandler handler = new MockHttpURLStreamHandler(HttpURLConnection.HTTP_OK, TEST_SAVE_PROPERTIES_FILE);
1522         final URL url = new URL(null, "http://jakarta.apache.org", handler);
1523         new FileHandler(conf).save(url);
1524         final MockHttpURLConnection con = handler.getMockConnection();
1525         assertTrue(con.getDoOutput());
1526         assertEquals("PUT", con.getRequestMethod());
1527         checkSavedConfig();
1528     }
1529 
1530     /**
1531      * Tests if the base path is taken into account by the save() method.
1532      */
1533     @Test
1534     void testSaveWithBasePath() throws Exception {
1535         conf.setProperty("test", "true");
1536         final FileHandler handler = new FileHandler(conf);
1537         handler.setBasePath(TEST_SAVE_PROPERTIES_FILE.getParentFile().toURI().toURL().toString());
1538         handler.setFileName(TEST_SAVE_PROPERTIES_FILE.getName());
1539         handler.save();
1540         assertTrue(TEST_SAVE_PROPERTIES_FILE.exists());
1541     }
1542 
1543     /**
1544      * Tests adding properties through a DataConfiguration. This is related to CONFIGURATION-332.
1545      */
1546     @Test
1547     void testSaveWithDataConfig() throws ConfigurationException {
1548         conf = new PropertiesConfiguration();
1549         final FileHandler handler = new FileHandler(conf);
1550         handler.setFile(TEST_SAVE_PROPERTIES_FILE);
1551         final DataConfiguration dataConfig = new DataConfiguration(conf);
1552         dataConfig.setProperty("foo", "bar");
1553         assertEquals("bar", conf.getString("foo"));
1554 
1555         handler.save();
1556         final PropertiesConfiguration config2 = new PropertiesConfiguration();
1557         load(config2, TEST_SAVE_PROPERTIES_FILE.getAbsolutePath());
1558         assertEquals("bar", config2.getString("foo"));
1559     }
1560 
1561     /**
1562      * Tests whether saving works correctly with the default list delimiter handler implementation.
1563      */
1564     @Test
1565     void testSaveWithDefaultListDelimiterHandler() throws ConfigurationException {
1566         conf.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
1567         saveTestConfig();
1568 
1569         final PropertiesConfiguration checkConfig = new PropertiesConfiguration();
1570         checkConfig.setListDelimiterHandler(conf.getListDelimiterHandler());
1571         new FileHandler(checkConfig).load(TEST_SAVE_PROPERTIES_FILE);
1572         ConfigurationAssert.assertConfigurationEquals(conf, checkConfig);
1573     }
1574 
1575     /**
1576      * Tests saving a configuration if delimiter parsing is disabled.
1577      */
1578     @Test
1579     void testSaveWithDelimiterParsingDisabled() throws ConfigurationException {
1580         conf.clear();
1581         conf.setListDelimiterHandler(new DisabledListDelimiterHandler());
1582         conf.addProperty("test.list", "a,b,c");
1583         conf.addProperty("test.dirs", "C:\\Temp\\,D:\\Data\\");
1584         saveTestConfig();
1585 
1586         final PropertiesConfiguration checkConfig = new PropertiesConfiguration();
1587         checkConfig.setListDelimiterHandler(new DisabledListDelimiterHandler());
1588         new FileHandler(checkConfig).load(TEST_SAVE_PROPERTIES_FILE);
1589         ConfigurationAssert.assertConfigurationEquals(conf, checkConfig);
1590     }
1591 
1592     /**
1593      * Tests whether write access to the footer comment is synchronized.
1594      */
1595     @Test
1596     void testSetFooterSynchronized() {
1597         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
1598         conf.setSynchronizer(sync);
1599         conf.setFooter("new comment");
1600         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
1601     }
1602 
1603     /**
1604      * Tests whether write access to the header comment is synchronized.
1605      */
1606     @Test
1607     void testSetHeaderSynchronized() {
1608         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
1609         conf.setSynchronizer(sync);
1610         conf.setHeader("new comment");
1611         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
1612     }
1613 
1614     @Test
1615     void testSetInclude() throws Exception {
1616         conf.clear();
1617         // change the include key
1618         PropertiesConfiguration.setInclude("import");
1619 
1620         // load the configuration
1621         load(conf, TEST_PROPERTIES);
1622 
1623         // restore the previous value for the other tests
1624         PropertiesConfiguration.setInclude("include");
1625 
1626         assertNull(conf.getString("include.loaded"));
1627     }
1628 
1629     /**
1630      * Tests setting the IOFactory to null. This should cause an exception.
1631      */
1632     @Test
1633     void testSetIOFactoryNull() {
1634         assertThrows(IllegalArgumentException.class, () -> conf.setIOFactory(null));
1635     }
1636 
1637     /**
1638      * Tests setting an IOFactory that uses a specialized reader.
1639      */
1640     @Test
1641     void testSetIOFactoryReader() throws ConfigurationException {
1642         final int propertyCount = 10;
1643         conf.clear();
1644         conf.setIOFactory(new PropertiesConfiguration.IOFactory() {
1645             @Override
1646             public PropertiesConfiguration.PropertiesReader createPropertiesReader(final Reader in) {
1647                 return new PropertiesReaderTestImpl(in, propertyCount);
1648             }
1649 
1650             @Override
1651             public PropertiesConfiguration.PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler) {
1652                 throw new UnsupportedOperationException("Unexpected call.");
1653             }
1654         });
1655         load(conf, TEST_PROPERTIES);
1656         for (int i = 1; i <= propertyCount; i++) {
1657             assertEquals(PROP_VALUE + i, conf.getString(PROP_NAME + i), "Wrong property value at " + i);
1658         }
1659     }
1660 
1661     /**
1662      * Tests setting an IOFactory that uses a specialized writer.
1663      */
1664     @Test
1665     void testSetIOFactoryWriter() throws ConfigurationException, IOException {
1666         final MutableObject<Writer> propertiesWriter = new MutableObject<>();
1667         conf.setIOFactory(new PropertiesConfiguration.IOFactory() {
1668             @Override
1669             public PropertiesConfiguration.PropertiesReader createPropertiesReader(final Reader in) {
1670                 throw new UnsupportedOperationException("Unexpected call.");
1671             }
1672 
1673             @Override
1674             public PropertiesConfiguration.PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler) {
1675                 try {
1676                     final PropertiesWriterTestImpl propWriter = new PropertiesWriterTestImpl(handler);
1677                     propertiesWriter.setValue(propWriter);
1678                     return propWriter;
1679                 } catch (final IOException e) {
1680                     return null;
1681                 }
1682             }
1683         });
1684         new FileHandler(conf).save(new StringWriter());
1685         propertiesWriter.getValue().close();
1686         checkSavedConfig();
1687     }
1688 
1689     /**
1690      * Tests whether a list property is handled correctly if delimiter parsing is disabled. This test is related to
1691      * CONFIGURATION-495.
1692      */
1693     @Test
1694     void testSetPropertyListWithDelimiterParsingDisabled() throws ConfigurationException {
1695         final String prop = "delimiterListProp";
1696         conf.setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE);
1697         final List<String> list = Arrays.asList("val", "val2", "val3");
1698         conf.setProperty(prop, list);
1699         saveTestConfig();
1700         conf.clear();
1701         load(conf, TEST_SAVE_PROPERTIES_FILE.getAbsolutePath());
1702         assertEquals(list, conf.getProperty(prop));
1703     }
1704 
1705     /**
1706      * Tests whether properties with slashes in their values can be saved. This test is related to CONFIGURATION-408.
1707      */
1708     @Test
1709     void testSlashEscaping() throws ConfigurationException {
1710         conf.setProperty(PROP_NAME, "http://www.apache.org");
1711         final StringWriter writer = new StringWriter();
1712         new FileHandler(conf).save(writer);
1713         final String s = writer.toString();
1714         assertTrue(s.contains(PROP_NAME + " = http://www.apache.org"));
1715     }
1716 
1717     /**
1718      * Tests whether special characters in a property value are un-escaped. This test is related to CONFIGURATION-640.
1719      */
1720     @Test
1721     void testUnEscapeCharacters() {
1722         assertEquals("#1 =: me!", conf.getString("test.unescape.characters"));
1723     }
1724 
1725     @Test
1726     void testUnescapeJava() {
1727         assertEquals("test\\,test", PropertiesConfiguration.unescapeJava("test\\,test"));
1728     }
1729 
1730     /**
1731      * Tests whether a footer comment is correctly written out.
1732      */
1733     @Test
1734     void testWriteFooterComment() throws ConfigurationException, IOException {
1735         final String footer = "my footer";
1736         conf.clear();
1737         conf.setProperty(PROP_NAME, PROP_VALUE);
1738         conf.setFooter(footer);
1739         final StringWriter out = new StringWriter();
1740         conf.write(out);
1741         assertEquals(PROP_NAME + " = " + PROP_VALUE + CR + "# " + footer + CR, out.toString());
1742     }
1743 }