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