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