View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration2;
19  
20  import static org.apache.commons.configuration2.TempDirUtils.newFile;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
24  import static org.junit.jupiter.api.Assertions.assertNotNull;
25  import static org.junit.jupiter.api.Assertions.assertNull;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  
29  import java.io.ByteArrayInputStream;
30  import java.io.File;
31  import java.io.FileOutputStream;
32  import java.io.IOException;
33  import java.io.StringReader;
34  import java.io.StringWriter;
35  import java.nio.charset.StandardCharsets;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.List;
40  
41  import javax.xml.parsers.DocumentBuilder;
42  import javax.xml.parsers.DocumentBuilderFactory;
43  import javax.xml.parsers.ParserConfigurationException;
44  import javax.xml.transform.Transformer;
45  import javax.xml.transform.TransformerFactoryConfigurationError;
46  
47  import org.apache.commons.configuration2.SynchronizerTestImpl.Methods;
48  import org.apache.commons.configuration2.builder.FileBasedBuilderParametersImpl;
49  import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
50  import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
51  import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
52  import org.apache.commons.configuration2.ex.ConfigurationException;
53  import org.apache.commons.configuration2.io.FileHandler;
54  import org.apache.commons.configuration2.resolver.CatalogResolver;
55  import org.apache.commons.configuration2.tree.ImmutableNode;
56  import org.apache.commons.configuration2.tree.NodeStructureHelper;
57  import org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine;
58  import org.apache.commons.lang3.StringUtils;
59  import org.junit.jupiter.api.BeforeEach;
60  import org.junit.jupiter.api.Test;
61  import org.junit.jupiter.api.io.TempDir;
62  import org.w3c.dom.Document;
63  import org.xml.sax.SAXException;
64  import org.xml.sax.SAXParseException;
65  import org.xml.sax.helpers.DefaultHandler;
66  
67  /**
68   * test for loading and saving XML properties files
69   */
70  public class TestXMLConfiguration {
71      /**
72       * A thread used for testing concurrent access to a builder.
73       */
74      private static final class ReloadThread extends Thread {
75          private final FileBasedConfigurationBuilder<?> builder;
76  
77          ReloadThread(final FileBasedConfigurationBuilder<?> confBulder) {
78              builder = confBulder;
79          }
80  
81          @Override
82          public void run() {
83              for (int i = 0; i < LOOP_COUNT; i++) {
84                  builder.resetResult();
85              }
86          }
87      }
88  
89      /** XML Catalog */
90      private static final String CATALOG_FILES = ConfigurationAssert.getTestFile("catalog.xml").getAbsolutePath();
91  
92      /** Constant for the used encoding. */
93      static final String ENCODING = StandardCharsets.ISO_8859_1.name();
94  
95      /** Constant for the test system ID. */
96      static final String SYSTEM_ID = "properties.dtd";
97  
98      /** Constant for the test public ID. */
99      static final String PUBLIC_ID = "-//Commons Configuration//DTD Test Configuration 1.3//EN";
100 
101     /** Constant for the DOCTYPE declaration. */
102     static final String DOCTYPE_DECL = " PUBLIC \"" + PUBLIC_ID + "\" \"" + SYSTEM_ID + "\">";
103 
104     /** Constant for the DOCTYPE prefix. */
105     static final String DOCTYPE = "<!DOCTYPE ";
106 
107     /** Constant for the transformer factory property. */
108     static final String PROP_FACTORY = "javax.xml.transform.TransformerFactory";
109 
110     /** Constant for the number of test threads. */
111     private static final int THREAD_COUNT = 5;
112     /** Constant for the number of loops in tests with multiple threads. */
113     private static final int LOOP_COUNT = 100;
114 
115     /**
116      * Creates a new XMLConfiguration and loads the specified file.
117      *
118      * @param fileName the name of the file to be loaded
119      * @return the newly created configuration instance
120      * @throws ConfigurationException if an error occurs
121      */
122     private static XMLConfiguration createFromFile(final String fileName) throws ConfigurationException {
123         final XMLConfiguration config = new XMLConfiguration();
124         config.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
125         load(config, fileName);
126         return config;
127     }
128 
129     /**
130      * Helper method for loading the specified configuration file.
131      *
132      * @param config the configuration
133      * @param fileName the name of the file to be loaded
134      * @throws ConfigurationException if an error occurs
135      */
136     private static void load(final XMLConfiguration config, final String fileName) throws ConfigurationException {
137         final FileHandler handler = new FileHandler(config);
138         handler.setFileName(fileName);
139         handler.load();
140     }
141 
142     /** A folder for temporary files. */
143     @TempDir
144     public File tempFolder;
145 
146     /** The File that we test with */
147     private final String testProperties = ConfigurationAssert.getTestFile("test.xml").getAbsolutePath();
148 
149     private final String testProperties2 = ConfigurationAssert.getTestFile("testDigesterConfigurationInclude1.xml").getAbsolutePath();
150 
151     private File testSaveConf;
152 
153     private File testSaveFile;
154 
155     private final String testFile2 = ConfigurationAssert.getTestFile("sample.xml").getAbsolutePath();
156 
157     private XMLConfiguration conf;
158 
159     /**
160      * Helper method for testing whether a configuration was correctly saved to the default output file.
161      *
162      * @return the newly loaded configuration
163      * @throws ConfigurationException if an error occurs
164      */
165     private XMLConfiguration checkSavedConfig() throws ConfigurationException {
166         return checkSavedConfig(testSaveConf);
167     }
168 
169     /**
170      * Tests whether the saved configuration file matches the original data.
171      *
172      * @param saveFile the saved configuration file
173      * @return the newly loaded configuration
174      * @throws ConfigurationException if an error occurs
175      */
176     private XMLConfiguration checkSavedConfig(final File saveFile) throws ConfigurationException {
177         final XMLConfiguration config = createFromFile(saveFile.getAbsolutePath());
178         ConfigurationAssert.assertConfigurationEquals(conf, config);
179         return config;
180     }
181 
182     /**
183      * Helper method for testing saving and loading a configuration when delimiter parsing is disabled.
184      *
185      * @param key the key to be checked
186      * @throws ConfigurationException if an error occurs
187      */
188     private void checkSaveDelimiterParsingDisabled(final String key) throws ConfigurationException {
189         conf.clear();
190         conf.setListDelimiterHandler(new DisabledListDelimiterHandler());
191         load(conf, testProperties);
192         conf.setProperty(key, "C:\\Temp\\,C:\\Data\\");
193         conf.addProperty(key, "a,b,c");
194         saveTestConfig();
195         final XMLConfiguration checkConf = new XMLConfiguration();
196         checkConf.setListDelimiterHandler(conf.getListDelimiterHandler());
197         load(checkConf, testSaveConf.getAbsolutePath());
198         ConfigurationAssert.assertConfigurationEquals(conf, checkConf);
199     }
200 
201     /**
202      * Creates a validating document builder.
203      *
204      * @return the document builder
205      * @throws ParserConfigurationException if an error occurs
206      */
207     private DocumentBuilder createValidatingDocBuilder() throws ParserConfigurationException {
208         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
209         factory.setValidating(true);
210         final DocumentBuilder builder = factory.newDocumentBuilder();
211         builder.setErrorHandler(new DefaultHandler() {
212             @Override
213             public void error(final SAXParseException ex) throws SAXException {
214                 throw ex;
215             }
216         });
217         return builder;
218     }
219 
220     private Document parseXml(final String xml) throws SAXException, IOException, ParserConfigurationException {
221         return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
222     }
223 
224     /**
225      * Removes the test output file if it exists.
226      */
227     private void removeTestFile() {
228         if (testSaveConf.exists()) {
229             assertTrue(testSaveConf.delete());
230         }
231     }
232 
233     /**
234      * Helper method for saving the test configuration to the default output file.
235      *
236      * @throws ConfigurationException if an error occurs
237      */
238     private void saveTestConfig() throws ConfigurationException {
239         final FileHandler handler = new FileHandler(conf);
240         handler.save(testSaveConf);
241     }
242 
243     @BeforeEach
244     public void setUp() throws Exception {
245         testSaveConf = newFile("testsave.xml", tempFolder);
246         testSaveFile = newFile("testsample2.xml", tempFolder);
247         conf = createFromFile(testProperties);
248         removeTestFile();
249     }
250 
251     @Test
252     void testAddList() {
253         conf.addProperty("test.array", "value1");
254         conf.addProperty("test.array", "value2");
255 
256         final List<Object> list = conf.getList("test.array");
257         assertEquals(Arrays.asList("value1", "value2"), list);
258     }
259 
260     /**
261      * Tests saving a configuration after a node was added. Test for CONFIGURATION-294.
262      */
263     @Test
264     void testAddNodesAndSave() throws ConfigurationException {
265         final ImmutableNode.Builder bldrNode = new ImmutableNode.Builder(1);
266         bldrNode.addChild(NodeStructureHelper.createNode("child", null));
267         bldrNode.addAttribute("attr", "");
268         final ImmutableNode node2 = NodeStructureHelper.createNode("test2", null);
269         conf.addNodes("add.nodes", Arrays.asList(bldrNode.name("test").create(), node2));
270         saveTestConfig();
271         conf.setProperty("add.nodes.test", "true");
272         conf.setProperty("add.nodes.test.child", "yes");
273         conf.setProperty("add.nodes.test[@attr]", "existing");
274         conf.setProperty("add.nodes.test2", "anotherValue");
275         saveTestConfig();
276         final XMLConfiguration c2 = new XMLConfiguration();
277         load(c2, testSaveConf.getAbsolutePath());
278         assertEquals("true", c2.getString("add.nodes.test"));
279         assertEquals("yes", c2.getString("add.nodes.test.child"));
280         assertEquals("existing", c2.getString("add.nodes.test[@attr]"));
281         assertEquals("anotherValue", c2.getString("add.nodes.test2"));
282     }
283 
284     /**
285      * Tests adding nodes from another configuration.
286      */
287     @Test
288     void testAddNodesCopy() throws ConfigurationException {
289         final XMLConfiguration c2 = new XMLConfiguration();
290         load(c2, testProperties2);
291         conf.addNodes("copiedProperties", c2.getModel().getNodeHandler().getRootNode().getChildren());
292         saveTestConfig();
293         checkSavedConfig();
294     }
295 
296     /**
297      * Tests whether it is possible to add nodes to a XMLConfiguration through a SubnodeConfiguration and whether these
298      * nodes have the correct type. This test is related to CONFIGURATION-472.
299      */
300     @Test
301     void testAddNodesToSubnodeConfiguration() throws Exception {
302         final HierarchicalConfiguration<ImmutableNode> sub = conf.configurationAt("element2", true);
303         sub.addProperty("newKey", "newvalue");
304         assertEquals("newvalue", conf.getString("element2.newKey"));
305     }
306 
307     @Test
308     void testAddObjectAttribute() {
309         conf.addProperty("test.boolean[@value]", Boolean.TRUE);
310         assertTrue(conf.getBoolean("test.boolean[@value]"));
311     }
312 
313     @Test
314     void testAddObjectProperty() {
315         // add a non string property
316         conf.addProperty("test.boolean", Boolean.TRUE);
317         assertTrue(conf.getBoolean("test.boolean"));
318     }
319 
320     @Test
321     void testAddProperty() {
322         // add a property to a non initialized XML configuration
323         final XMLConfiguration config = new XMLConfiguration();
324         config.addProperty("test.string", "hello");
325 
326         assertEquals("hello", config.getString("test.string"));
327     }
328 
329     /**
330      * Tests whether list properties are added correctly if delimiter parsing is disabled. This test is related to
331      * CONFIGURATION-495.
332      */
333     @Test
334     void testAddPropertyListWithDelimiterParsingDisabled() throws ConfigurationException {
335         conf.clear();
336         final String prop = "delimiterListProp";
337         conf.setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE);
338         final List<String> list = Arrays.asList("val", "val2", "val3");
339         conf.addProperty(prop, list);
340         saveTestConfig();
341         final XMLConfiguration conf2 = new XMLConfiguration();
342         load(conf2, testSaveConf.getAbsolutePath());
343         assertEquals(list, conf2.getProperty(prop));
344     }
345 
346     /**
347      * Tests if a second file can be appended to a first.
348      */
349     @Test
350     void testAppend() throws Exception {
351         load(conf, testProperties2);
352         assertEquals("value", conf.getString("element"));
353         assertEquals("tasks", conf.getString("table.name"));
354 
355         saveTestConfig();
356         conf = createFromFile(testSaveConf.getAbsolutePath());
357         assertEquals("value", conf.getString("element"));
358         assertEquals("tasks", conf.getString("table.name"));
359         assertEquals("application", conf.getString("table[@tableType]"));
360     }
361 
362     /**
363      * Tries to create an attribute with multiple values. Only the first value is taken into account.
364      */
365     @Test
366     void testAttributeKeyWithMultipleValues() throws ConfigurationException {
367         conf.addProperty("errorTest[@multiAttr]", Arrays.asList("v1", "v2"));
368         saveTestConfig();
369         final XMLConfiguration checkConfig = new XMLConfiguration();
370         load(checkConfig, testSaveConf.getAbsolutePath());
371         assertEquals("v1", checkConfig.getString("errorTest[@multiAttr]"));
372     }
373 
374     /**
375      * Tests whether the addNodes() method triggers an auto save.
376      */
377     @Test
378     void testAutoSaveAddNodes() throws ConfigurationException {
379         final FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class);
380         builder.configure(new FileBasedBuilderParametersImpl().setFileName(testProperties));
381         conf = builder.getConfiguration();
382         builder.getFileHandler().setFile(testSaveConf);
383         builder.setAutoSave(true);
384         final ImmutableNode node = NodeStructureHelper.createNode("addNodesTest", Boolean.TRUE);
385         final Collection<ImmutableNode> nodes = new ArrayList<>(1);
386         nodes.add(node);
387         conf.addNodes("test.autosave", nodes);
388         final XMLConfiguration c2 = new XMLConfiguration();
389         load(c2, testSaveConf.getAbsolutePath());
390         assertTrue(c2.getBoolean("test.autosave.addNodesTest"));
391     }
392 
393     /**
394      * Tests whether the auto save mechanism is triggered by changes at a subnode configuration.
395      */
396     @Test
397     void testAutoSaveWithSubnodeConfig() throws ConfigurationException {
398         final FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class);
399         builder.configure(new FileBasedBuilderParametersImpl().setFileName(testProperties));
400         conf = builder.getConfiguration();
401         builder.getFileHandler().setFile(testSaveConf);
402         builder.setAutoSave(true);
403         final String newValue = "I am autosaved";
404         final Configuration sub = conf.configurationAt("element2.subelement", true);
405         sub.setProperty("subsubelement", newValue);
406         assertEquals(newValue, conf.getString("element2.subelement.subsubelement"));
407         final XMLConfiguration conf2 = new XMLConfiguration();
408         load(conf2, testSaveConf.getAbsolutePath());
409         assertEquals(newValue, conf2.getString("element2.subelement.subsubelement"));
410     }
411 
412     /**
413      * Tests whether a subnode configuration created from another subnode configuration of a XMLConfiguration can trigger
414      * the auto save mechanism.
415      */
416     @Test
417     void testAutoSaveWithSubSubnodeConfig() throws ConfigurationException {
418         final FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class);
419         builder.configure(new FileBasedBuilderParametersImpl().setFileName(testProperties));
420         conf = builder.getConfiguration();
421         builder.getFileHandler().setFile(testSaveConf);
422         builder.setAutoSave(true);
423         final String newValue = "I am autosaved";
424         final HierarchicalConfiguration<?> sub1 = conf.configurationAt("element2", true);
425         final HierarchicalConfiguration<?> sub2 = sub1.configurationAt("subelement", true);
426         sub2.setProperty("subsubelement", newValue);
427         assertEquals(newValue, conf.getString("element2.subelement.subsubelement"));
428         final XMLConfiguration conf2 = new XMLConfiguration();
429         load(conf2, testSaveConf.getAbsolutePath());
430         assertEquals(newValue, conf2.getString("element2.subelement.subsubelement"));
431     }
432 
433     @Test
434     void testClearAttributeMultipleDisjoined() throws Exception {
435         String key = "clear.list.item[@id]";
436         conf.clearProperty(key);
437         assertNull(conf.getProperty(key));
438         assertNull(conf.getProperty(key));
439         key = "clear.list.item";
440         assertNotNull(conf.getProperty(key));
441         assertNotNull(conf.getProperty(key));
442     }
443 
444     @Test
445     void testClearAttributeNonExisting() {
446         final String key = "clear[@id]";
447         conf.clearProperty(key);
448         assertNull(conf.getProperty(key));
449         assertNull(conf.getProperty(key));
450     }
451 
452     @Test
453     void testClearAttributeSingle() {
454         String key = "clear.element2[@id]";
455         conf.clearProperty(key);
456         assertNull(conf.getProperty(key));
457         assertNull(conf.getProperty(key));
458         key = "clear.element2";
459         assertNotNull(conf.getProperty(key));
460         assertNotNull(conf.getProperty(key));
461     }
462 
463     @Test
464     void testClearPropertyCData() {
465         final String key = "clear.cdata";
466         conf.clearProperty(key);
467         assertNull(conf.getProperty(key));
468         assertNull(conf.getProperty(key));
469     }
470 
471     @Test
472     void testClearPropertyMultipleDisjoined() throws Exception {
473         final String key = "list.item";
474         conf.clearProperty(key);
475         assertNull(conf.getProperty(key));
476         assertNull(conf.getProperty(key));
477     }
478 
479     @Test
480     void testClearPropertyMultipleSiblings() {
481         String key = "clear.list.item";
482         conf.clearProperty(key);
483         assertNull(conf.getProperty(key));
484         assertNull(conf.getProperty(key));
485         key = "clear.list.item[@id]";
486         assertNotNull(conf.getProperty(key));
487         assertNotNull(conf.getProperty(key));
488     }
489 
490     @Test
491     void testClearPropertyNonText() {
492         final String key = "clear.comment";
493         conf.clearProperty(key);
494         assertNull(conf.getProperty(key));
495         assertNull(conf.getProperty(key));
496     }
497 
498     @Test
499     void testClearPropertyNotExisting() {
500         final String key = "clearly";
501         conf.clearProperty(key);
502         assertNull(conf.getProperty(key));
503         assertNull(conf.getProperty(key));
504     }
505 
506     @Test
507     void testClearPropertySingleElement() {
508         final String key = "clear.element";
509         conf.clearProperty(key);
510         assertNull(conf.getProperty(key));
511         assertNull(conf.getProperty(key));
512     }
513 
514     @Test
515     void testClearPropertySingleElementWithAttribute() {
516         String key = "clear.element2";
517         conf.clearProperty(key);
518         assertNull(conf.getProperty(key));
519         assertNull(conf.getProperty(key));
520         key = "clear.element2[@id]";
521         assertNotNull(conf.getProperty(key));
522         assertNotNull(conf.getProperty(key));
523     }
524 
525     /**
526      * Tests removing the text of the root element.
527      */
528     @Test
529     void testClearTextRootElement() throws ConfigurationException {
530         final String xml = "<e a=\"v\">text</e>";
531         conf.clear();
532         final StringReader in = new StringReader(xml);
533         final FileHandler handler = new FileHandler(conf);
534         handler.load(in);
535         assertEquals("text", conf.getString(""));
536 
537         conf.clearProperty("");
538         saveTestConfig();
539         checkSavedConfig();
540     }
541 
542     /**
543      * Tests the clone() method.
544      */
545     @Test
546     void testClone() {
547         final Configuration c = (Configuration) conf.clone();
548         final XMLConfiguration copy = assertInstanceOf(XMLConfiguration.class, c);
549         assertNotNull(conf.getDocument());
550         assertNull(copy.getDocument());
551 
552         copy.setProperty("element3", "clonedValue");
553         assertEquals("value", conf.getString("element3"));
554         conf.setProperty("element3[@name]", "originalFoo");
555         assertEquals("foo", copy.getString("element3[@name]"));
556     }
557 
558     /**
559      * Tests saving a configuration after cloning to ensure that the clone and the original are completely detached.
560      */
561     @Test
562     void testCloneWithSave() throws ConfigurationException {
563         final XMLConfiguration c = (XMLConfiguration) conf.clone();
564         c.addProperty("test.newProperty", Boolean.TRUE);
565         conf.addProperty("test.orgProperty", Boolean.TRUE);
566         new FileHandler(c).save(testSaveConf);
567         final XMLConfiguration c2 = new XMLConfiguration();
568         load(c2, testSaveConf.getAbsolutePath());
569         assertTrue(c2.getBoolean("test.newProperty"));
570         assertFalse(c2.containsKey("test.orgProperty"));
571     }
572 
573     /**
574      * Tests access to tag names with delimiter characters.
575      */
576     @Test
577     void testComplexNames() {
578         assertEquals("Name with dot", conf.getString("complexNames.my..elem"));
579         assertEquals("Another dot", conf.getString("complexNames.my..elem.sub..elem"));
580     }
581 
582     @Test
583     void testConcurrentGetAndReload() throws ConfigurationException, InterruptedException {
584         final FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class);
585         builder.configure(new FileBasedBuilderParametersImpl().setFileName(testProperties));
586         XMLConfiguration config = builder.getConfiguration();
587         assertNotNull(config.getProperty("test.short"));
588 
589         final Thread[] testThreads = new Thread[THREAD_COUNT];
590         for (int i = 0; i < testThreads.length; ++i) {
591             testThreads[i] = new ReloadThread(builder);
592             testThreads[i].start();
593         }
594 
595         for (int i = 0; i < LOOP_COUNT; i++) {
596             config = builder.getConfiguration();
597             assertNotNull(config.getProperty("test.short"));
598         }
599 
600         for (final Thread testThread : testThreads) {
601             testThread.join();
602         }
603     }
604 
605     /**
606      * Tests the copy constructor for null input.
607      */
608     @Test
609     void testCopyNull() {
610         conf = new XMLConfiguration(null);
611         assertTrue(conf.isEmpty());
612         assertEquals("configuration", conf.getRootElementName());
613     }
614 
615     /**
616      * Tests whether the name of the root element is copied when a configuration is created using the copy constructor.
617      */
618     @Test
619     void testCopyRootName() throws ConfigurationException {
620         final String rootName = "rootElement";
621         final String xml = "<" + rootName + "><test>true</test></" + rootName + ">";
622         conf.clear();
623         new FileHandler(conf).load(new StringReader(xml));
624         XMLConfiguration copy = new XMLConfiguration(conf);
625         assertEquals(rootName, copy.getRootElementName());
626         new FileHandler(copy).save(testSaveConf);
627         copy = new XMLConfiguration();
628         load(copy, testSaveConf.getAbsolutePath());
629         assertEquals(rootName, copy.getRootElementName());
630     }
631 
632     /**
633      * Tests whether the name of the root element is copied for a configuration for which not yet a document exists.
634      */
635     @Test
636     void testCopyRootNameNoDocument() throws ConfigurationException {
637         final String rootName = "rootElement";
638         conf = new XMLConfiguration();
639         conf.setRootElementName(rootName);
640         conf.setProperty("test", Boolean.TRUE);
641         final XMLConfiguration copy = new XMLConfiguration(conf);
642         assertEquals(rootName, copy.getRootElementName());
643         new FileHandler(copy).save(testSaveConf);
644         load(copy, testSaveConf.getAbsolutePath());
645         assertEquals(rootName, copy.getRootElementName());
646     }
647 
648     /**
649      * Tests setting a custom document builder.
650      */
651     @Test
652     void testCustomDocBuilder() throws Exception {
653         // Load an invalid XML file with the default (non validating)
654         // doc builder. This should work...
655         conf = new XMLConfiguration();
656         load(conf, ConfigurationAssert.getTestFile("testValidateInvalid.xml").getAbsolutePath());
657         assertEquals("customers", conf.getString("table.name"));
658         assertFalse(conf.containsKey("table.fields.field(1).type"));
659     }
660 
661     /**
662      * Tests whether a validating document builder detects a validation error.
663      */
664     @Test
665     void testCustomDocBuilderValidationError() throws Exception {
666         final DocumentBuilder builder = createValidatingDocBuilder();
667         conf = new XMLConfiguration();
668         conf.setDocumentBuilder(builder);
669         final String fileName = ConfigurationAssert.getTestFile("testValidateInvalid.xml").getAbsolutePath();
670         assertThrows(ConfigurationException.class, () -> load(conf, fileName));
671     }
672 
673     /**
674      * Tests whether a valid document can be loaded with a validating document builder.
675      */
676     @Test
677     void testCustomDocBuilderValidationSuccess() throws Exception {
678         final DocumentBuilder builder = createValidatingDocBuilder();
679         conf = new XMLConfiguration();
680         conf.setDocumentBuilder(builder);
681         load(conf, ConfigurationAssert.getTestFile("testValidateValid.xml").getAbsolutePath());
682         assertTrue(conf.containsKey("table.fields.field(1).type"));
683     }
684 
685     /**
686      * Tests string properties with list delimiters when delimiter parsing is disabled
687      */
688     @Test
689     void testDelimiterParsingDisabled() throws ConfigurationException {
690         final XMLConfiguration conf2 = new XMLConfiguration();
691         load(conf2, testProperties);
692 
693         assertEquals("a,b,c", conf2.getString("split.list3[@values]"));
694         assertEquals(0, conf2.getMaxIndex("split.list3[@values]"));
695         assertEquals("a\\,b\\,c", conf2.getString("split.list4[@values]"));
696         assertEquals("a,b,c", conf2.getString("split.list1"));
697         assertEquals(0, conf2.getMaxIndex("split.list1"));
698         assertEquals("a\\,b\\,c", conf2.getString("split.list2"));
699     }
700 
701     /**
702      * Tests whether string properties with list delimiters can be accessed if delimiter parsing is disabled and the XPath
703      * expression engine is used.
704      */
705     @Test
706     void testDelimiterParsingDisabledXPath() throws ConfigurationException {
707         final XMLConfiguration conf2 = new XMLConfiguration();
708         conf2.setExpressionEngine(new XPathExpressionEngine());
709         load(conf2, testProperties);
710 
711         assertEquals("a,b,c", conf2.getString("split/list3/@values"));
712         assertEquals(0, conf2.getMaxIndex("split/list3/@values"));
713         assertEquals("a\\,b\\,c", conf2.getString("split/list4/@values"));
714         assertEquals("a,b,c", conf2.getString("split/list1"));
715         assertEquals(0, conf2.getMaxIndex("split/list1"));
716         assertEquals("a\\,b\\,c", conf2.getString("split/list2"));
717     }
718 
719     /**
720      * Tests whether a DTD can be accessed.
721      */
722     @Test
723     void testDtd() throws ConfigurationException {
724         conf = new XMLConfiguration();
725         load(conf, "testDtd.xml");
726         assertEquals("value1", conf.getString("entry(0)"));
727         assertEquals("test2", conf.getString("entry(1)[@key]"));
728     }
729 
730     /**
731      * Tests whether an attribute can be set to an empty string. This test is related to CONFIGURATION-446.
732      */
733     @Test
734     void testEmptyAttribute() throws ConfigurationException {
735         final String key = "element3[@value]";
736         conf.setProperty(key, "");
737         assertTrue(conf.containsKey(key));
738         assertEquals("", conf.getString(key));
739         saveTestConfig();
740         conf = new XMLConfiguration();
741         load(conf, testSaveConf.getAbsolutePath());
742         assertTrue(conf.containsKey(key));
743         assertEquals("", conf.getString(key));
744     }
745 
746     /**
747      * Tests handling of empty elements.
748      */
749     @Test
750     void testEmptyElements() throws ConfigurationException {
751         assertTrue(conf.containsKey("empty"));
752         assertEquals("", conf.getString("empty"));
753         conf.addProperty("empty2", "");
754         conf.setProperty("empty", "no more empty");
755         saveTestConfig();
756 
757         conf = new XMLConfiguration();
758         load(conf, testSaveConf.getAbsolutePath());
759         assertEquals("no more empty", conf.getString("empty"));
760         assertEquals("", conf.getProperty("empty2"));
761     }
762 
763     /**
764      * Tests the isEmpty() method for an empty configuration that was reloaded.
765      */
766     @Test
767     void testEmptyReload() throws ConfigurationException {
768         conf = new XMLConfiguration();
769         assertTrue(conf.isEmpty());
770         saveTestConfig();
771         load(conf, testSaveConf.getAbsolutePath());
772         assertTrue(conf.isEmpty());
773     }
774 
775     @Test
776     void testGetAttribute() {
777         assertEquals("foo", conf.getProperty("element3[@name]"));
778     }
779 
780     @Test
781     void testGetCommentedProperty() {
782         assertEquals("", conf.getProperty("test.comment"));
783     }
784 
785     @Test
786     void testGetComplexProperty() {
787         assertEquals("I'm complex!", conf.getProperty("element2.subelement.subsubelement"));
788     }
789 
790     @Test
791     void testgetProperty() {
792         // test non-leaf element
793         Object property = conf.getProperty("clear");
794         assertNull(property);
795 
796         // test non-existent element
797         property = conf.getProperty("e");
798         assertNull(property);
799 
800         // test non-existent element
801         property = conf.getProperty("element3[@n]");
802         assertNull(property);
803 
804         // test single element
805         property = conf.getProperty("element");
806         assertInstanceOf(String.class, property);
807         assertEquals("value", property);
808 
809         // test single attribute
810         property = conf.getProperty("element3[@name]");
811         assertInstanceOf(String.class, property);
812         assertEquals("foo", property);
813 
814         // test non-text/cdata element
815         property = conf.getProperty("test.comment");
816         assertEquals("", property);
817 
818         // test cdata element
819         property = conf.getProperty("test.cdata");
820         assertInstanceOf(String.class, property);
821         assertEquals("<cdata value>", property);
822 
823         // test multiple sibling elements
824         property = conf.getProperty("list.sublist.item");
825         List<?> list = assertInstanceOf(List.class, property);
826         assertEquals(Arrays.asList("five", "six"), list);
827 
828         // test multiple, disjoined elements
829         property = conf.getProperty("list.item");
830         list = assertInstanceOf(List.class, property);
831         assertEquals(Arrays.asList("one", "two", "three", "four"), list);
832 
833         // test multiple, disjoined attributes
834         property = conf.getProperty("list.item[@name]");
835         list = assertInstanceOf(List.class, property);
836         assertEquals(Arrays.asList("one", "three"), list);
837     }
838 
839     @Test
840     void testGetProperty() {
841         assertEquals("value", conf.getProperty("element"));
842     }
843 
844     @Test
845     void testGetPropertyWithXMLEntity() {
846         assertEquals("1<2", conf.getProperty("test.entity"));
847     }
848 
849     /**
850      * Tests the copy constructor.
851      */
852     @Test
853     void testInitCopy() throws ConfigurationException {
854         final XMLConfiguration copy = new XMLConfiguration(conf);
855         copy.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
856         assertEquals("value", copy.getProperty("element"));
857         assertNull(copy.getDocument());
858 
859         new FileHandler(copy).save(testSaveConf);
860         checkSavedConfig();
861     }
862 
863     /**
864      * Tests list nodes with multiple values and attributes.
865      */
866     @Test
867     void testListWithAttributes() {
868         assertEquals(6, conf.getList("attrList.a").size());
869         assertEquals("ABC", conf.getString("attrList.a(0)"));
870         assertEquals("x", conf.getString("attrList.a(0)[@name]"));
871         assertEquals(6, conf.getList("attrList.a[@name]").size());
872     }
873 
874     /**
875      * Tests a list node with attributes that has multiple values separated by the list delimiter. In this scenario the
876      * attribute should be added to all list nodes.
877      */
878     @Test
879     void testListWithAttributesMultiValue() {
880         assertEquals("1", conf.getString("attrList.a(1)"));
881         assertEquals("y", conf.getString("attrList.a(1)[@name]"));
882         for (int i = 1; i <= 3; i++) {
883             assertEquals(i, conf.getInt("attrList.a(" + i + ")"));
884             assertEquals("y", conf.getString("attrList.a(" + i + ")[@name]"));
885         }
886     }
887 
888     /**
889      * Tests a list node with multiple values and multiple attributes. All attribute values should be assigned to all list
890      * nodes.
891      */
892     @Test
893     void testListWithMultipleAttributesMultiValue() {
894         for (int i = 1; i <= 2; i++) {
895             final String idxStr = String.format("(%d)", Integer.valueOf(i + 3));
896             final String nodeKey = "attrList.a" + idxStr;
897             assertEquals("value" + i, conf.getString(nodeKey));
898             assertEquals("u", conf.getString(nodeKey + "[@name]"));
899             assertEquals("yes", conf.getString(nodeKey + "[@test]"));
900         }
901     }
902 
903     /**
904      * Tests constructing an XMLConfiguration from a non existing file and later saving to this file.
905      */
906     @Test
907     void testLoadAndSaveFromFile() throws Exception {
908         // If the file does not exist, an empty config is created
909         assertFalse(testSaveConf.exists());
910         final FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class, null, true);
911         builder.configure(new FileBasedBuilderParametersImpl().setFile(testSaveConf));
912         conf = builder.getConfiguration();
913         assertTrue(conf.isEmpty());
914         conf.addProperty("test", "yes");
915         builder.save();
916 
917         final XMLConfiguration checkConfig = createFromFile(testSaveConf.getAbsolutePath());
918         assertEquals("yes", checkConfig.getString("test"));
919     }
920 
921     @Test
922     void testLoadChildNamespace() throws ConfigurationException {
923         conf = new XMLConfiguration();
924         new FileHandler(conf).load(ConfigurationAssert.getTestFile("testChildNamespace.xml"));
925         assertEquals("http://example.com/", conf.getString("foo:bar.[@xmlns:foo]"));
926     }
927 
928     /**
929      * Tests loading from a stream.
930      */
931     @Test
932     void testLoadFromStream() throws Exception {
933         final String xml = "<?xml version=\"1.0\"?><config><test>1</test></config>";
934         conf = new XMLConfiguration();
935         FileHandler handler = new FileHandler(conf);
936         handler.load(new ByteArrayInputStream(xml.getBytes()));
937         assertEquals(1, conf.getInt("test"));
938 
939         conf = new XMLConfiguration();
940         handler = new FileHandler(conf);
941         handler.load(new ByteArrayInputStream(xml.getBytes()), "UTF8");
942         assertEquals(1, conf.getInt("test"));
943     }
944 
945     /**
946      * Tests loading a non well formed XML from a string.
947      */
948     @Test
949     void testLoadInvalidXML() throws Exception {
950         final String xml = "<?xml version=\"1.0\"?><config><test>1</rest></config>";
951         conf = new XMLConfiguration();
952         final FileHandler handler = new FileHandler(conf);
953         final StringReader reader = new StringReader(xml);
954         assertThrows(ConfigurationException.class, () -> handler.load(reader));
955     }
956 
957     /**
958      * Tests whether the encoding is correctly detected by the XML parser. This is done by loading an XML file with the
959      * encoding "UTF-16". If this encoding is not detected correctly, an exception will be thrown that "Content is not
960      * allowed in prolog". This test case is related to issue 34204.
961      */
962     @Test
963     void testLoadWithEncoding() throws ConfigurationException {
964         conf = new XMLConfiguration();
965         new FileHandler(conf).load(ConfigurationAssert.getTestFile("testEncoding.xml"));
966         assertEquals("test3_yoge", conf.getString("yoge"));
967     }
968 
969     @Test
970     void testLoadWithRootNamespace() throws ConfigurationException {
971         conf = new XMLConfiguration();
972         new FileHandler(conf).load(ConfigurationAssert.getTestFile("testRootNamespace.xml"));
973         assertEquals("http://example.com/", conf.getString("[@xmlns:foo]"));
974     }
975 
976     /**
977      * Tests that attribute values are not split.
978      */
979     @Test
980     void testNoDelimiterParsingInAttrValues() throws ConfigurationException {
981         conf.clear();
982         load(conf, testProperties);
983         final List<Object> expr = conf.getList("expressions[@value]");
984         assertEquals(Arrays.asList("a || (b && c) | !d"), expr);
985     }
986 
987     /**
988      * Tests whether an attribute value can be overridden.
989      */
990     @Test
991     void testOverrideAttribute() {
992         conf.addProperty("element3[@name]", "bar");
993 
994         final List<Object> list = conf.getList("element3[@name]");
995         assertEquals(Arrays.asList("bar"), list);
996     }
997 
998     /**
999      * Tests whether spaces are preserved when the xml:space attribute is set.
1000      */
1001     @Test
1002     void testPreserveSpace() {
1003         assertEquals(" ", conf.getString("space.blank"));
1004         assertEquals(" * * ", conf.getString("space.stars"));
1005     }
1006 
1007     /**
1008      * Tests an xml:space attribute with an invalid value. This will be interpreted as default.
1009      */
1010     @Test
1011     void testPreserveSpaceInvalid() {
1012         assertEquals("Some other text", conf.getString("space.testInvalid"));
1013     }
1014 
1015     /**
1016      * Tests whether the xml:space attribute works directly on the current element. This test is related to
1017      * CONFIGURATION-555.
1018      */
1019     @Test
1020     void testPreserveSpaceOnElement() {
1021         assertEquals(" preserved ", conf.getString("spaceElement"));
1022         assertEquals("   ", conf.getString("spaceBlankElement"));
1023     }
1024 
1025     /**
1026      * Tests whether the xml:space attribute can be overridden in nested elements.
1027      */
1028     @Test
1029     void testPreserveSpaceOverride() {
1030         assertEquals("Some text", conf.getString("space.description"));
1031     }
1032 
1033     /**
1034      * Tests whether the public ID is accessed in a synchronized manner.
1035      */
1036     @Test
1037     void testPublicIdSynchronized() {
1038         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
1039         conf.setSynchronizer(sync);
1040         conf.setPublicID(PUBLIC_ID);
1041         assertEquals(PUBLIC_ID, conf.getPublicID());
1042         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_READ, Methods.END_READ);
1043     }
1044 
1045     /**
1046      * Tests a direct invocation of the read() method. This is not allowed because certain initializations have not been
1047      * done. This test is related to CONFIGURATION-641.
1048      */
1049     @Test
1050     void testReadCalledDirectly() {
1051         conf = new XMLConfiguration();
1052         final String content = "<configuration><test>1</test></configuration>";
1053         final ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes());
1054         final ConfigurationException e = assertThrows(ConfigurationException.class, () -> conf.read(bis));
1055         assertTrue(e.getMessage().contains("FileHandler"));
1056     }
1057 
1058     @Test
1059     void testSave() throws Exception {
1060         // add an array of strings to the configuration
1061         conf.addProperty("string", "value1");
1062         for (int i = 1; i < 5; i++) {
1063             conf.addProperty("test.array", "value" + i);
1064         }
1065 
1066         // add comma delimited lists with escaped delimiters
1067         conf.addProperty("split.list5", "a\\,b\\,c");
1068         conf.setProperty("element3", "value\\,value1\\,value2");
1069         conf.setProperty("element3[@name]", "foo\\,bar");
1070 
1071         // save the configuration
1072         saveTestConfig();
1073 
1074         // read the configuration and compare the properties
1075         checkSavedConfig();
1076     }
1077 
1078     /**
1079      * Tests saving a configuration that was created from a hierarchical configuration. This test exposes bug
1080      * CONFIGURATION-301.
1081      */
1082     @Test
1083     void testSaveAfterCreateWithCopyConstructor() throws ConfigurationException {
1084         final HierarchicalConfiguration<ImmutableNode> hc = conf.configurationAt("element2");
1085         conf = new XMLConfiguration(hc);
1086         saveTestConfig();
1087         final XMLConfiguration checkConfig = checkSavedConfig();
1088         assertEquals("element2", checkConfig.getRootElementName());
1089     }
1090 
1091     /**
1092      * Tests saving attributes (related to issue 34442).
1093      */
1094     @Test
1095     void testSaveAttributes() throws Exception {
1096         conf.clear();
1097         load(conf, testProperties);
1098         saveTestConfig();
1099         conf = new XMLConfiguration();
1100         load(conf, testSaveConf.getAbsolutePath());
1101         assertEquals("foo", conf.getString("element3[@name]"));
1102     }
1103 
1104     /**
1105      * Tests saving and loading a configuration when delimiter parsing is disabled.
1106      */
1107     @Test
1108     void testSaveDelimiterParsingDisabled() throws ConfigurationException {
1109         checkSaveDelimiterParsingDisabled("list.delimiter.test");
1110     }
1111 
1112     /**
1113      * Tests saving to a stream.
1114      */
1115     @Test
1116     void testSaveToStream() throws ConfigurationException, IOException {
1117         final FileHandler handler = new FileHandler(conf);
1118         try (FileOutputStream out = new FileOutputStream(testSaveConf)) {
1119             handler.save(out, "UTF8");
1120         }
1121 
1122         checkSavedConfig(testSaveConf);
1123     }
1124 
1125     /**
1126      * Tests whether a configuration can be saved to a stream with a specific encoding.
1127      */
1128     @Test
1129     void testSaveToStreamWithEncoding() throws ConfigurationException, IOException {
1130         final FileHandler handler = new FileHandler(conf);
1131         handler.setEncoding("UTF8");
1132         try (FileOutputStream out = new FileOutputStream(testSaveConf)) {
1133             handler.save(out);
1134         }
1135 
1136         checkSavedConfig(testSaveConf);
1137     }
1138 
1139     /**
1140      * Tests saving to a URL.
1141      */
1142     @Test
1143     void testSaveToURL() throws Exception {
1144         final FileHandler handler = new FileHandler(conf);
1145         handler.save(testSaveConf.toURI().toURL());
1146         checkSavedConfig(testSaveConf);
1147     }
1148 
1149     /**
1150      * Tests whether a windows path can be saved correctly. This test is related to CONFIGURATION-428.
1151      */
1152     @Test
1153     void testSaveWindowsPath() throws ConfigurationException {
1154         conf.clear();
1155         conf.setListDelimiterHandler(new DisabledListDelimiterHandler());
1156         conf.addProperty("path", "C:\\Temp");
1157         final StringWriter writer = new StringWriter();
1158         new FileHandler(conf).save(writer);
1159         final String content = writer.toString();
1160         assertTrue(content.contains("<path>C:\\Temp</path>"), "Path not found: ");
1161         saveTestConfig();
1162         final XMLConfiguration conf2 = new XMLConfiguration();
1163         load(conf2, testSaveConf.getAbsolutePath());
1164         assertEquals("C:\\Temp", conf2.getString("path"));
1165     }
1166 
1167     /**
1168      * Tests string properties with list delimiters when delimiter parsing is disabled
1169      */
1170     @Test
1171     void testSaveWithDelimiterParsingDisabled() throws ConfigurationException {
1172         conf = new XMLConfiguration();
1173         conf.setExpressionEngine(new XPathExpressionEngine());
1174         load(conf, testProperties);
1175 
1176         assertEquals("a,b,c", conf.getString("split/list3/@values"));
1177         assertEquals(0, conf.getMaxIndex("split/list3/@values"));
1178         assertEquals("a\\,b\\,c", conf.getString("split/list4/@values"));
1179         assertEquals("a,b,c", conf.getString("split/list1"));
1180         assertEquals(0, conf.getMaxIndex("split/list1"));
1181         assertEquals("a\\,b\\,c", conf.getString("split/list2"));
1182         // save the configuration
1183         saveTestConfig();
1184 
1185         XMLConfiguration config = new XMLConfiguration();
1186         // config.setExpressionEngine(new XPathExpressionEngine());
1187         load(config, testFile2);
1188         config.setProperty("Employee[@attr1]", "3,2,1");
1189         assertEquals("3,2,1", config.getString("Employee[@attr1]"));
1190         new FileHandler(config).save(testSaveFile);
1191         config = new XMLConfiguration();
1192         // config.setExpressionEngine(new XPathExpressionEngine());
1193         load(config, testSaveFile.getAbsolutePath());
1194         config.setProperty("Employee[@attr1]", "1,2,3");
1195         assertEquals("1,2,3", config.getString("Employee[@attr1]"));
1196         config.setProperty("Employee[@attr2]", "one, two, three");
1197         assertEquals("one, two, three", config.getString("Employee[@attr2]"));
1198         config.setProperty("Employee.text", "a,b,d");
1199         assertEquals("a,b,d", config.getString("Employee.text"));
1200         config.setProperty("Employee.Salary", "100,000");
1201         assertEquals("100,000", config.getString("Employee.Salary"));
1202         new FileHandler(config).save(testSaveFile);
1203         final XMLConfiguration checkConfig = new XMLConfiguration();
1204         checkConfig.setExpressionEngine(new XPathExpressionEngine());
1205         load(checkConfig, testSaveFile.getAbsolutePath());
1206         assertEquals("1,2,3", checkConfig.getString("Employee/@attr1"));
1207         assertEquals("one, two, three", checkConfig.getString("Employee/@attr2"));
1208         assertEquals("a,b,d", checkConfig.getString("Employee/text"));
1209         assertEquals("100,000", checkConfig.getString("Employee/Salary"));
1210     }
1211 
1212     /**
1213      * Tests whether the DOCTYPE survives a save operation.
1214      */
1215     @Test
1216     void testSaveWithDoctype() throws ConfigurationException {
1217         conf = new XMLConfiguration();
1218         load(conf, "testDtdPublic.xml");
1219 
1220         assertEquals(PUBLIC_ID, conf.getPublicID());
1221         assertEquals(SYSTEM_ID, conf.getSystemID());
1222         final StringWriter out = new StringWriter();
1223         new FileHandler(conf).save(out);
1224         assertTrue(out.toString().contains(DOCTYPE));
1225     }
1226 
1227     /**
1228      * Tests setting public and system IDs for the DOCTYPE and then saving the configuration. This should generate a DOCTYPE
1229      * declaration.
1230      */
1231     @Test
1232     void testSaveWithDoctypeIDs() throws ConfigurationException {
1233         assertNull(conf.getPublicID());
1234         assertNull(conf.getSystemID());
1235         conf.setPublicID(PUBLIC_ID);
1236         conf.setSystemID(SYSTEM_ID);
1237         final StringWriter out = new StringWriter();
1238         new FileHandler(conf).save(out);
1239         assertTrue(out.toString().contains(DOCTYPE + "testconfig" + DOCTYPE_DECL));
1240     }
1241 
1242     /**
1243      * Tests whether the encoding is written to the generated XML file.
1244      */
1245     @Test
1246     void testSaveWithEncoding() throws ConfigurationException {
1247         conf = new XMLConfiguration();
1248         conf.setProperty("test", "a value");
1249         final FileHandler handler = new FileHandler(conf);
1250         handler.setEncoding(ENCODING);
1251 
1252         final StringWriter out = new StringWriter();
1253         handler.save(out);
1254         assertTrue(out.toString().contains("encoding=\"" + ENCODING + "\""));
1255     }
1256 
1257     /**
1258      * Tests saving a configuration if an invalid transformer factory is specified. In this case an error is thrown by the
1259      * transformer factory. XMLConfiguration should not catch this error.
1260      */
1261     @Test
1262     void testSaveWithInvalidTransformerFactory() {
1263         System.setProperty(PROP_FACTORY, "an.invalid.Class");
1264         try {
1265             assertThrows(TransformerFactoryConfigurationError.class, this::saveTestConfig);
1266         } finally {
1267             System.getProperties().remove(PROP_FACTORY);
1268         }
1269     }
1270 
1271     /**
1272      * Tests whether a default encoding is used if no specific encoding is set. According to the XSLT specification
1273      * (http://www.w3.org/TR/xslt#output) this should be either UTF-8 or UTF-16.
1274      */
1275     @Test
1276     void testSaveWithNullEncoding() throws ConfigurationException {
1277         conf = new XMLConfiguration();
1278         conf.setProperty("testNoEncoding", "yes");
1279         final FileHandler handler = new FileHandler(conf);
1280 
1281         final StringWriter out = new StringWriter();
1282         handler.save(out);
1283         assertTrue(out.toString().contains("encoding=\"UTF-"), "Encoding was written to file");
1284     }
1285 
1286     @Test
1287     void testSaveWithRootAttributes() throws ConfigurationException {
1288         conf.setProperty("[@xmlns:ex]", "http://example.com/");
1289         assertEquals("http://example.com/", conf.getString("[@xmlns:ex]"));
1290         final FileHandler handler = new FileHandler(conf);
1291 
1292         final StringWriter out = new StringWriter();
1293         handler.save(out);
1294         assertTrue(out.toString().contains("testconfig xmlns:ex=\"http://example.com/\""), "Encoding was not written to file");
1295     }
1296 
1297     @Test
1298     void testSaveWithRootAttributesByHand() throws ConfigurationException {
1299         conf = new XMLConfiguration();
1300         conf.addProperty("[@xmlns:foo]", "http://example.com/");
1301         assertEquals("http://example.com/", conf.getString("[@xmlns:foo]"));
1302         final FileHandler handler = new FileHandler(conf);
1303 
1304         final StringWriter out = new StringWriter();
1305         handler.save(out);
1306         assertTrue(out.toString().contains("configuration xmlns:foo=\"http://example.com/\""), "Encoding was not written to file");
1307     }
1308 
1309     /**
1310      * Tests modifying an XML document and saving it with schema validation enabled.
1311      */
1312     @Test
1313     void testSaveWithValidation() throws Exception {
1314         final CatalogResolver resolver = new CatalogResolver();
1315         resolver.setCatalogFiles(CATALOG_FILES);
1316         conf = new XMLConfiguration();
1317         conf.setEntityResolver(resolver);
1318         conf.setSchemaValidation(true);
1319         load(conf, testFile2);
1320         conf.setProperty("Employee.SSN", "123456789");
1321         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
1322         conf.setSynchronizer(sync);
1323         conf.validate();
1324         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
1325         saveTestConfig();
1326         conf = new XMLConfiguration();
1327         load(conf, testSaveConf.getAbsolutePath());
1328         assertEquals("123456789", conf.getString("Employee.SSN"));
1329     }
1330 
1331     /**
1332      * Tests modifying an XML document and validating it against the schema.
1333      */
1334     @Test
1335     void testSaveWithValidationFailure() throws Exception {
1336         final CatalogResolver resolver = new CatalogResolver();
1337         resolver.setCatalogFiles(CATALOG_FILES);
1338         conf = new XMLConfiguration();
1339         conf.setEntityResolver(resolver);
1340         conf.setSchemaValidation(true);
1341         load(conf, testFile2);
1342         conf.setProperty("Employee.Email", "JohnDoe@test.org");
1343         final Exception e = assertThrows(Exception.class, conf::validate);
1344         final Throwable cause = e.getCause();
1345         assertInstanceOf(SAXParseException.class, cause);
1346     }
1347 
1348     @Test
1349     void testSetAttribute() {
1350         // replace an existing attribute
1351         conf.setProperty("element3[@name]", "bar");
1352         assertEquals("bar", conf.getProperty("element3[@name]"));
1353 
1354         // set a new attribute
1355         conf.setProperty("foo[@bar]", "value");
1356         assertEquals("value", conf.getProperty("foo[@bar]"));
1357 
1358         conf.setProperty("name1", "value1");
1359         assertEquals("value1", conf.getProperty("name1"));
1360     }
1361 
1362     @Test
1363     void testSetProperty() throws Exception {
1364         conf.setProperty("element.string", "hello");
1365 
1366         assertEquals("hello", conf.getString("element.string"));
1367         assertEquals("hello", conf.getProperty("element.string"));
1368     }
1369 
1370     /**
1371      * Tests whether list properties are set correctly if delimiter parsing is disabled. This test is related to
1372      * CONFIGURATION-495.
1373      */
1374     @Test
1375     void testSetPropertyListWithDelimiterParsingDisabled() throws ConfigurationException {
1376         final String prop = "delimiterListProp";
1377         final List<String> list = Arrays.asList("val", "val2", "val3");
1378         conf.setProperty(prop, list);
1379         saveTestConfig();
1380         final XMLConfiguration conf2 = new XMLConfiguration();
1381         load(conf2, testSaveConf.getAbsolutePath());
1382         assertEquals(list, conf2.getProperty(prop));
1383     }
1384 
1385     /**
1386      * Tests setting an attribute on the root element.
1387      */
1388     @Test
1389     void testSetRootAttribute() throws ConfigurationException {
1390         conf.setProperty("[@test]", "true");
1391         assertEquals("true", conf.getString("[@test]"));
1392         saveTestConfig();
1393         XMLConfiguration checkConf = checkSavedConfig();
1394         assertTrue(checkConf.containsKey("[@test]"));
1395         checkConf.setProperty("[@test]", "newValue");
1396         conf = checkConf;
1397         saveTestConfig();
1398         checkConf = checkSavedConfig();
1399         assertEquals("newValue", checkConf.getString("[@test]"));
1400     }
1401 
1402     @Test
1403     void testSetRootNamespace() throws ConfigurationException {
1404         conf.addProperty("[@xmlns:foo]", "http://example.com/");
1405         conf.addProperty("foo:bar", "foobar");
1406         assertEquals("http://example.com/", conf.getString("[@xmlns:foo]"));
1407         saveTestConfig();
1408         final XMLConfiguration checkConf = checkSavedConfig();
1409         assertTrue(checkConf.containsKey("[@xmlns:foo]"));
1410         checkConf.setProperty("[@xmlns:foo]", "http://example.net/");
1411     }
1412 
1413     /**
1414      * Tests setting text of the root element.
1415      */
1416     @Test
1417     void testSetTextRootElement() throws ConfigurationException {
1418         conf.setProperty("", "Root text");
1419         saveTestConfig();
1420         checkSavedConfig();
1421     }
1422 
1423     /**
1424      * Tests string properties with list delimiters and escaped delimiters.
1425      */
1426     @Test
1427     void testSplitLists() {
1428         assertEquals("a,b,c", conf.getString("split.list3[@values]"));
1429         assertEquals(0, conf.getMaxIndex("split.list3[@values]"));
1430         assertEquals("a\\,b\\,c", conf.getString("split.list4[@values]"));
1431         assertEquals("a", conf.getString("split.list1"));
1432         assertEquals(2, conf.getMaxIndex("split.list1"));
1433         assertEquals("a,b,c", conf.getString("split.list2"));
1434     }
1435 
1436     /**
1437      * Tests the subset() method. There was a bug that calling subset() had undesired side effects.
1438      */
1439     @Test
1440     void testSubset() throws ConfigurationException {
1441         conf = new XMLConfiguration();
1442         load(conf, "testHierarchicalXMLConfiguration.xml");
1443         conf.subset("tables.table(0)");
1444         saveTestConfig();
1445 
1446         conf = new XMLConfiguration();
1447         load(conf, "testHierarchicalXMLConfiguration.xml");
1448         assertEquals("users", conf.getString("tables.table(0).name"));
1449     }
1450 
1451     /**
1452      * Tests whether the system ID is accessed in a synchronized manner.
1453      */
1454     @Test
1455     void testSystemIdSynchronized() {
1456         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
1457         conf.setSynchronizer(sync);
1458         conf.setSystemID(SYSTEM_ID);
1459         assertEquals(SYSTEM_ID, conf.getSystemID());
1460         sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_READ, Methods.END_READ);
1461     }
1462 
1463     /**
1464      * Tests DTD validation using the setValidating() method.
1465      */
1466     @Test
1467     void testValidating() throws ConfigurationException {
1468         final File nonValidFile = ConfigurationAssert.getTestFile("testValidateInvalid.xml");
1469         conf = new XMLConfiguration();
1470         assertFalse(conf.isValidating());
1471 
1472         // Load a non valid XML document. Should work for isValidating() == false
1473         load(conf, nonValidFile.getAbsolutePath());
1474         assertEquals("customers", conf.getString("table.name"));
1475         assertFalse(conf.containsKey("table.fields.field(1).type"));
1476     }
1477 
1478     /**
1479      * Tests whether an invalid file is detected when validating is enabled.
1480      */
1481     @Test
1482     void testValidatingInvalidFile() {
1483         conf = new XMLConfiguration();
1484         conf.setValidating(true);
1485         assertThrows(ConfigurationException.class, () -> load(conf, "testValidateInvalid.xml"));
1486     }
1487 
1488     @Test
1489     void testWrite() throws Exception {
1490         final XMLConfiguration xmlConfig = new XMLConfiguration();
1491         xmlConfig.setRootElementName("IAmRoot");
1492         final StringWriter sw = new StringWriter();
1493         xmlConfig.write(sw);
1494         // Check that we can parse the XML.
1495         assertNotNull(parseXml(sw.toString()));
1496     }
1497 
1498     @Test
1499     void testWriteIndentSize() throws Exception {
1500         final XMLConfiguration xmlConfig = new XMLConfiguration();
1501         xmlConfig.setRootElementName("IAmRoot");
1502         final StringWriter sw = new StringWriter();
1503         xmlConfig.setProperty("Child", "Alexander");
1504         xmlConfig.write(sw);
1505         // Check that we can parse the XML.
1506         final String xml = sw.toString();
1507         assertNotNull(parseXml(xml));
1508         final String indent = StringUtils.repeat(' ', XMLConfiguration.DEFAULT_INDENT_SIZE);
1509         assertTrue(xml.contains(System.lineSeparator() + indent + "<Child>"));
1510     }
1511 
1512     @Test
1513     void testWriteWithTransformer() throws Exception {
1514         final XMLConfiguration xmlConfig = new XMLConfiguration();
1515         xmlConfig.setRootElementName("IAmRoot");
1516         xmlConfig.setProperty("Child", "Alexander");
1517         final StringWriter sw = new StringWriter();
1518         final Transformer transformer = xmlConfig.createTransformer();
1519         final int indentSize = 8;
1520         transformer.setOutputProperty(XMLConfiguration.INDENT_AMOUNT_PROPERTY, Integer.toString(indentSize));
1521         xmlConfig.write(sw, transformer);
1522         final String xml = sw.toString();
1523         assertNotNull(parseXml(xml));
1524         final String indent = StringUtils.repeat(' ', indentSize);
1525         assertTrue(xml.contains(System.lineSeparator() + indent + "<Child>"));
1526     }
1527 
1528     /**
1529      * Tests accessing properties when the XPATH expression engine is set.
1530      */
1531     @Test
1532     void testXPathExpressionEngine() {
1533         conf.setExpressionEngine(new XPathExpressionEngine());
1534         assertEquals("foo\"bar", conf.getString("test[1]/entity/@name"));
1535         conf.clear();
1536         assertNull(conf.getString("test[1]/entity/@name"));
1537     }
1538 }