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