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 this
4    * work for additional information regarding copyright ownership. The ASF
5    * licenses this file to You under the Apache License, Version 2.0 (the
6    * "License"); you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  
18  package org.apache.commons.configuration2;
19  
20  import static org.apache.commons.configuration2.TempDirUtils.newFile;
21  import static org.hamcrest.CoreMatchers.containsString;
22  import static org.hamcrest.MatcherAssert.assertThat;
23  import static org.junit.jupiter.api.Assertions.assertEquals;
24  import static org.junit.jupiter.api.Assertions.assertFalse;
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  import static org.junit.jupiter.api.Assertions.fail;
29  
30  import java.io.File;
31  import java.io.FileWriter;
32  import java.io.IOException;
33  import java.io.PrintWriter;
34  import java.io.StringReader;
35  import java.io.StringWriter;
36  import java.io.Writer;
37  import java.text.MessageFormat;
38  import java.util.Arrays;
39  import java.util.Collections;
40  import java.util.HashSet;
41  import java.util.Iterator;
42  import java.util.List;
43  import java.util.Set;
44  import java.util.stream.Stream;
45  
46  import org.apache.commons.configuration2.SynchronizerTestImpl.Methods;
47  import org.apache.commons.configuration2.builder.FileBasedBuilderParametersImpl;
48  import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
49  import org.apache.commons.configuration2.builder.fluent.Parameters;
50  import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
51  import org.apache.commons.configuration2.ex.ConfigurationException;
52  import org.apache.commons.configuration2.sync.ReadWriteSynchronizer;
53  import org.apache.commons.configuration2.tree.DefaultExpressionEngine;
54  import org.apache.commons.configuration2.tree.DefaultExpressionEngineSymbols;
55  import org.apache.commons.configuration2.tree.ImmutableNode;
56  import org.apache.commons.configuration2.tree.NodeHandler;
57  import org.apache.commons.configuration2.tree.NodeNameMatchers;
58  import org.junit.jupiter.api.Test;
59  import org.junit.jupiter.api.io.TempDir;
60  import org.junit.jupiter.params.ParameterizedTest;
61  import org.junit.jupiter.params.provider.Arguments;
62  import org.junit.jupiter.params.provider.MethodSource;
63  
64  /**
65   * Test class for {@code INIConfiguration}.
66   */
67  public class TestINIConfiguration {
68  
69      /**
70       * A thread class for testing concurrent access to the global section.
71       */
72      private static final class GlobalSectionTestThread extends Thread {
73  
74          /** The configuration. */
75          private final INIConfiguration config;
76  
77          /** A flag whether an error was found. */
78          volatile boolean error;
79  
80          /**
81           * Creates a new instance of {@code GlobalSectionTestThread} and initializes it.
82           *
83           * @param conf the configuration object
84           */
85          public GlobalSectionTestThread(final INIConfiguration conf) {
86              config = conf;
87          }
88  
89          /**
90           * Accesses the global section in a loop. If there is no correct synchronization, this can cause an exception.
91           */
92          @Override
93          public void run() {
94              final int loopCount = 250;
95  
96              for (int i = 0; i < loopCount && !error; i++) {
97                  try {
98                      config.getSection(null);
99                  } catch (final IllegalStateException istex) {
100                     error = true;
101                 }
102             }
103         }
104     }
105 
106     private static final String LINE_SEPARATOR = System.lineSeparator();
107 
108     /** Constant for the content of an ini file. */
109     private static final String INI_DATA = "[section1]" + LINE_SEPARATOR + "var1 = foo" + LINE_SEPARATOR + "var2 = 451" + LINE_SEPARATOR + LINE_SEPARATOR
110         + "[section2]" + LINE_SEPARATOR + "var1 = 123.45" + LINE_SEPARATOR + "var2 = bar" + LINE_SEPARATOR + LINE_SEPARATOR + "[section3]" + LINE_SEPARATOR
111         + "var1 = true" + LINE_SEPARATOR + "interpolated = ${section3.var1}" + LINE_SEPARATOR + "multi = foo" + LINE_SEPARATOR + "multi = bar" + LINE_SEPARATOR
112         + LINE_SEPARATOR;
113 
114     private static final String INI_DATA2 = "[section4]" + LINE_SEPARATOR + "var1 = \"quoted value\"" + LINE_SEPARATOR
115         + "var2 = \"quoted value\\nwith \\\"quotes\\\"\"" + LINE_SEPARATOR + "var3 = 123 ; comment" + LINE_SEPARATOR + "var4 = \"1;2;3\" ; comment"
116         + LINE_SEPARATOR + "var5 = '\\'quoted\\' \"value\"' ; comment" + LINE_SEPARATOR + "var6 = \"\"" + LINE_SEPARATOR;
117 
118     private static final String INI_DATA3 = "[section5]" + LINE_SEPARATOR + "multiLine = one \\" + LINE_SEPARATOR + "    two      \\" + LINE_SEPARATOR
119         + " three" + LINE_SEPARATOR + "singleLine = C:\\Temp\\" + LINE_SEPARATOR + "multiQuoted = one \\" + LINE_SEPARATOR + "\"  two  \" \\" + LINE_SEPARATOR
120         + "  three" + LINE_SEPARATOR + "multiComment = one \\ ; a comment" + LINE_SEPARATOR + "two" + LINE_SEPARATOR
121         + "multiQuotedComment = \" one \" \\ ; comment" + LINE_SEPARATOR + "two" + LINE_SEPARATOR + "noFirstLine = \\" + LINE_SEPARATOR + "  line 2"
122         + LINE_SEPARATOR + "continueNoLine = one \\" + LINE_SEPARATOR;
123 
124     private static final String INI_DATA4 = "[section6]" + LINE_SEPARATOR + "key1{0}value1" + LINE_SEPARATOR + "key2{0}value2" + LINE_SEPARATOR + LINE_SEPARATOR
125         + "[section7]" + LINE_SEPARATOR + "key3{0}value3" + LINE_SEPARATOR;
126 
127     private static final String INI_DATA5 = "[section4]" + LINE_SEPARATOR + "var1 = \"quoted value\"" + LINE_SEPARATOR
128         + "var2 = \"quoted value\\nwith \\\"quotes\\\"\"" + LINE_SEPARATOR + "var3 = 123 # comment" + LINE_SEPARATOR + "var4 = \"1#2;3\" # comment"
129         + LINE_SEPARATOR + "var5 = '\\'quoted\\' \"value\"' # comment" + LINE_SEPARATOR + "var6 = \"\"" + LINE_SEPARATOR;
130 
131     /** Constant for the content of an ini file - with section inline comment defined with semicolon */
132     private static final String INI_DATA6 = "[section1]; main section" + LINE_SEPARATOR + "var1 = foo" + LINE_SEPARATOR + LINE_SEPARATOR
133         + "[section11] ; sub-section related to [section1]" + LINE_SEPARATOR + "var1 = 123.45" + LINE_SEPARATOR;
134 
135     /** Constant for the content of an ini file - with section inline comment defined with number sign */
136     private static final String INI_DATA7 = "[section1]# main section" + LINE_SEPARATOR + "var1 = foo" + LINE_SEPARATOR + LINE_SEPARATOR
137         + "[section11] # sub-section related to [section1]" + LINE_SEPARATOR + "var1 = 123.45" + LINE_SEPARATOR;
138 
139     private static final String INI_DATA_SEPARATORS = "[section]" + LINE_SEPARATOR + "var1 = value1" + LINE_SEPARATOR + "var2 : value2" + LINE_SEPARATOR
140         + "var3=value3" + LINE_SEPARATOR + "var4:value4" + LINE_SEPARATOR + "var5 : value=5" + LINE_SEPARATOR + "var:6=value" + LINE_SEPARATOR
141         + "var:7=\"value7\"" + LINE_SEPARATOR + "var:8 =  \"value8\"" + LINE_SEPARATOR;
142 
143     /** An ini file that contains only a property in the global section. */
144     private static final String INI_DATA_GLOBAL_ONLY = "globalVar = testGlobal" + LINE_SEPARATOR + LINE_SEPARATOR;
145 
146     /** An ini file with a global section. */
147     private static final String INI_DATA_GLOBAL = INI_DATA_GLOBAL_ONLY + INI_DATA;
148 
149     /**
150      * Loads the specified content into the given configuration instance.
151      *
152      * @param instance the configuration
153      * @param data the data to be loaded
154      * @throws ConfigurationException if an error occurs
155      */
156     private static void load(final INIConfiguration instance, final String data) throws ConfigurationException {
157         try (StringReader reader = new StringReader(data)) {
158             instance.read(reader);
159         } catch (final IOException e) {
160             throw new ConfigurationException(e);
161         }
162     }
163 
164     /**
165      * Saves the specified configuration to a string. The string can be compared with an expected value or again loaded into
166      * a configuration.
167      *
168      * @param config the configuration to be saved
169      * @return the content of this configuration saved to a string
170      * @throws ConfigurationException if an error occurs
171      */
172     private static String saveToString(final INIConfiguration config) throws ConfigurationException {
173         final StringWriter writer = new StringWriter();
174         try {
175             config.write(writer);
176         } catch (final IOException e) {
177             throw new ConfigurationException(e);
178         }
179         return writer.toString();
180     }
181 
182     /**
183      * Creates a INIConfiguration object that is initialized from the given data.
184      *
185      * @param data the data of the configuration (an ini file as string)
186      * @return the initialized configuration
187      * @throws ConfigurationException if an error occurs
188      */
189     private static INIConfiguration setUpConfig(final String data) throws ConfigurationException {
190         return setUpConfig(data, false);
191     }
192 
193     /**
194      * Creates a INIConfiguration object that is initialized from the given data.
195      *
196      * @param data the data of the configuration (an ini file as string)
197      * @param inLineCommentsAllowed when true, inline comments on section line are allowed
198      * @return the initialized configuration
199      * @throws ConfigurationException if an error occurs
200      */
201     private static INIConfiguration setUpConfig(final String data, final boolean inLineCommentsAllowed) throws ConfigurationException {
202         // @formatter:off
203         final INIConfiguration instance = INIConfiguration.builder()
204                 .setSectionInLineCommentsAllowed(inLineCommentsAllowed)
205                 .build();
206         // @formatter:on
207         instance.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
208         load(instance, data);
209         return instance;
210     }
211 
212     /** A folder for temporary files. */
213     @TempDir
214     public File tempFolder;
215 
216     /**
217      * Tests the values of some properties to ensure that the configuration was correctly loaded.
218      *
219      * @param instance the configuration to check
220      */
221     private void checkContent(final INIConfiguration instance) {
222         assertEquals("foo", instance.getString("section1.var1"));
223         assertEquals(451, instance.getInt("section1.var2"));
224         assertEquals(123.45, instance.getDouble("section2.var1"), .001);
225         assertEquals("bar", instance.getString("section2.var2"));
226         assertTrue(instance.getBoolean("section3.var1"));
227         assertEquals(new HashSet<>(Arrays.asList("section1", "section2", "section3")), instance.getSections());
228     }
229 
230     /**
231      * Helper method for testing the load operation. Loads the specified content into a configuration and then checks some
232      * properties.
233      *
234      * @param data the data to load
235      */
236     private void checkLoad(final String data) throws ConfigurationException {
237         final INIConfiguration instance = setUpConfig(data);
238         checkContent(instance);
239     }
240 
241     /**
242      * Helper method for testing a save operation. This method constructs a configuration from the specified content string.
243      * Then it saves this configuration and checks whether the result matches the original content.
244      *
245      * @param content the content of the configuration
246      * @throws ConfigurationException if an error occurs
247      */
248     private void checkSave(final String content) throws ConfigurationException {
249         final INIConfiguration config = setUpConfig(content);
250         final String sOutput = saveToString(config);
251         assertEquals(content, sOutput);
252     }
253 
254     /**
255      * Tests whether the specified configuration contains exactly the expected sections.
256      *
257      * @param config the configuration to check
258      * @param expected an array with the expected sections
259      */
260     private void checkSectionNames(final INIConfiguration config, final String[] expected) {
261         final Set<String> sectionNames = config.getSections();
262         assertEquals(new HashSet<>(Arrays.asList(expected)), sectionNames);
263     }
264 
265     /**
266      * Tests the names of the sections returned by the configuration.
267      *
268      * @param data the data of the ini configuration
269      * @param expected the expected section names
270      * @return the configuration instance
271      */
272     private INIConfiguration checkSectionNames(final String data, final String[] expected) throws ConfigurationException {
273         final INIConfiguration config = setUpConfig(data);
274         checkSectionNames(config, expected);
275         return config;
276     }
277 
278     /**
279      * Test of read method with changed comment leading separator
280      */
281     @Test
282     public void testCommentLeadingSeparatorUsedInINIInput() throws Exception {
283         final String inputCommentLeadingSeparator = ";";
284         final String input = "[section]" + LINE_SEPARATOR + "key1=a;b;c" + LINE_SEPARATOR + "key2=a#b#c" + LINE_SEPARATOR + ";key3=value3" + LINE_SEPARATOR
285             + "#key4=value4" + LINE_SEPARATOR;
286 
287         final INIConfiguration instance = new FileBasedConfigurationBuilder<>(INIConfiguration.class)
288             .configure(new Parameters().ini().setCommentLeadingCharsUsedInInput(inputCommentLeadingSeparator)).getConfiguration();
289         load(instance, input);
290 
291         assertEquals("a;b;c", instance.getString("section.key1"));
292         assertEquals("a#b#c", instance.getString("section.key2"));
293         assertNull(instance.getString("section.;key3"));
294         assertEquals("value4", instance.getString("section.#key4"));
295     }
296 
297     /**
298      * Tests whether an expression engine can be used which ignores case.
299      */
300     @Test
301     public void testExpressionEngineIgnoringCase() throws ConfigurationException {
302         final DefaultExpressionEngine engine = new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS, NodeNameMatchers.EQUALS_IGNORE_CASE);
303         final INIConfiguration config = new INIConfiguration();
304         config.setExpressionEngine(engine);
305         load(config, INI_DATA);
306 
307         checkContent(config);
308         assertEquals("foo", config.getString("Section1.var1"));
309         assertEquals("foo", config.getString("section1.Var1"));
310         assertEquals("foo", config.getString("SECTION1.VAR1"));
311     }
312 
313     /**
314      * Tests a property that has no key.
315      */
316     @Test
317     public void testGetPropertyNoKey() throws ConfigurationException {
318         final String data = INI_DATA2 + LINE_SEPARATOR + "= noKey" + LINE_SEPARATOR;
319         final INIConfiguration config = setUpConfig(data);
320         assertEquals("noKey", config.getString("section4. "));
321     }
322 
323     /**
324      * Tests a property that has no value.
325      */
326     @Test
327     public void testGetPropertyNoValue() throws ConfigurationException {
328         final String data = INI_DATA2 + LINE_SEPARATOR + "noValue =" + LINE_SEPARATOR;
329         final INIConfiguration config = setUpConfig(data);
330         assertEquals("", config.getString("section4.noValue"));
331     }
332 
333     /**
334      * Tests whether the sub configuration returned by getSection() is connected to the parent.
335      */
336     @Test
337     public void testGetSectionConnected() throws ConfigurationException {
338         final INIConfiguration config = setUpConfig(INI_DATA);
339         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section1");
340         section.setProperty("var1", "foo2");
341         assertEquals("foo2", config.getString("section1.var1"));
342     }
343 
344     /**
345      * Tests whether getSection() can deal with duplicate sections.
346      */
347     @Test
348     public void testGetSectionDuplicate() {
349         final INIConfiguration config = new INIConfiguration();
350         config.addProperty("section.var1", "value1");
351         config.addProperty("section(-1).var2", "value2");
352         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section");
353         final Iterator<String> keys = section.getKeys();
354         assertEquals("var1", keys.next());
355         assertFalse(keys.hasNext());
356     }
357 
358     /**
359      * Tests querying the properties of an existing section.
360      */
361     @Test
362     public void testGetSectionExisting() throws ConfigurationException {
363         final INIConfiguration config = setUpConfig(INI_DATA);
364         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section1");
365         assertEquals("foo", section.getString("var1"));
366         assertEquals("451", section.getString("var2"));
367     }
368 
369     /**
370      * Tests concurrent access to the global section.
371      */
372     @Test
373     public void testGetSectionGloabalMultiThreaded() throws ConfigurationException, InterruptedException {
374         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
375         config.setSynchronizer(new ReadWriteSynchronizer());
376         final int threadCount = 10;
377         final GlobalSectionTestThread[] threads = new GlobalSectionTestThread[threadCount];
378         for (int i = 0; i < threadCount; i++) {
379             threads[i] = new GlobalSectionTestThread(config);
380             threads[i].start();
381         }
382         for (int i = 0; i < threadCount; i++) {
383             threads[i].join();
384             assertFalse(threads[i].error);
385         }
386     }
387 
388     /**
389      * Tests querying the content of the global section.
390      */
391     @Test
392     public void testGetSectionGlobal() throws ConfigurationException {
393         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
394         final HierarchicalConfiguration<ImmutableNode> section = config.getSection(null);
395         assertEquals("testGlobal", section.getString("globalVar"));
396     }
397 
398     /**
399      * Tests querying the content of the global section if there is none.
400      */
401     @Test
402     public void testGetSectionGlobalNonExisting() throws ConfigurationException {
403         final INIConfiguration config = setUpConfig(INI_DATA);
404         final HierarchicalConfiguration<ImmutableNode> section = config.getSection(null);
405         assertTrue(section.isEmpty());
406     }
407 
408     /**
409      * Tests querying the properties of a section that was merged from two sections with the same name.
410      */
411     @Test
412     public void testGetSectionMerged() throws ConfigurationException {
413         final String data = INI_DATA + "[section1]" + LINE_SEPARATOR + "var3 = merged" + LINE_SEPARATOR;
414         final INIConfiguration config = setUpConfig(data);
415         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section1");
416         assertEquals("foo", section.getString("var1"));
417         assertEquals("451", section.getString("var2"));
418         assertEquals("merged", section.getString("var3"));
419     }
420 
421     /**
422      * Tests querying a non existing section.
423      */
424     @Test
425     public void testGetSectionNonExisting() throws ConfigurationException {
426         final INIConfiguration config = setUpConfig(INI_DATA);
427         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("Non existing section");
428         assertTrue(section.isEmpty());
429     }
430 
431     /**
432      * Tests whether a section that was created by getSection() can be manipulated.
433      */
434     @Test
435     public void testGetSectionNonExistingManipulate() throws ConfigurationException, IOException {
436         final INIConfiguration config = setUpConfig(INI_DATA);
437         HierarchicalConfiguration<ImmutableNode> section = config.getSection("newSection");
438         section.addProperty("test", "success");
439         assertEquals("success", config.getString("newSection.test"));
440         final StringWriter writer = new StringWriter();
441         config.write(writer);
442         final INIConfiguration config2 = setUpConfig(writer.toString());
443         section = config2.getSection("newSection");
444         assertEquals("success", section.getString("test"));
445     }
446 
447     /**
448      * Test of getSections method, of class {@link INIConfiguration} .
449      */
450     @Test
451     public void testGetSections() {
452         final INIConfiguration instance = new INIConfiguration();
453         instance.addProperty("test1.foo", "bar");
454         instance.addProperty("test2.foo", "abc");
455         final Set<String> expResult = new HashSet<>();
456         expResult.add("test1");
457         expResult.add("test2");
458         final Set<String> result = instance.getSections();
459         assertEquals(expResult, result);
460     }
461 
462     /**
463      * Tests whether a section added later is also found by getSections().
464      */
465     @Test
466     public void testGetSectionsAdded() throws ConfigurationException {
467         final INIConfiguration config = setUpConfig(INI_DATA2);
468         config.addProperty("section5.test", Boolean.TRUE);
469         checkSectionNames(config, new String[] {"section4", "section5"});
470     }
471 
472     /**
473      * Tests whether variables containing a dot are not misinterpreted as sections. This test is related to
474      * CONFIGURATION-327.
475      */
476     @Test
477     public void testGetSectionsDottedVar() throws ConfigurationException {
478         final String data = "dotted.var = 1" + LINE_SEPARATOR + INI_DATA_GLOBAL;
479         final INIConfiguration config = checkSectionNames(data, new String[] {null, "section1", "section2", "section3"});
480         assertEquals(1, config.getInt("dotted..var"));
481     }
482 
483     /**
484      * Tests whether the sections of a configuration can be queried that contains only a global section.
485      */
486     @Test
487     public void testGetSectionsGlobalOnly() throws ConfigurationException {
488         checkSectionNames(INI_DATA_GLOBAL_ONLY, new String[] {null});
489     }
490 
491     /**
492      * Tests querying the sections if there is no global section.
493      */
494     @Test
495     public void testGetSectionsNoGlobal() throws ConfigurationException {
496         checkSectionNames(INI_DATA, new String[] {"section1", "section2", "section3"});
497     }
498 
499     /**
500      * Tests whether synchronization is performed when querying the configuration's sections.
501      */
502     @Test
503     public void testGetSectionsSynchronized() throws ConfigurationException {
504         final INIConfiguration config = setUpConfig(INI_DATA);
505         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
506         config.setSynchronizer(sync);
507         assertFalse(config.getSections().isEmpty());
508         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
509     }
510 
511     /**
512      * Tests querying the sections if a global section if available.
513      */
514     @Test
515     public void testGetSectionsWithGlobal() throws ConfigurationException {
516         checkSectionNames(INI_DATA_GLOBAL, new String[] {null, "section1", "section2", "section3"});
517     }
518 
519     /**
520      * Tests reading a property from the global section.
521      */
522     @Test
523     public void testGlobalProperty() throws ConfigurationException {
524         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
525         assertEquals("testGlobal", config.getString("globalVar"));
526     }
527 
528     /**
529      * Tests whether the sub configuration for the global section is connected to its parent.
530      */
531     @Test
532     public void testGlobalSectionConnected() throws ConfigurationException {
533         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
534         final HierarchicalConfiguration<ImmutableNode> sub = config.getSection(null);
535         config.setProperty("globalVar", "changed");
536         assertEquals("changed", sub.getString("globalVar"));
537     }
538 
539     /**
540      * Tests whether the node handler of a global section correctly returns a child by index.
541      */
542     @Test
543     public void testGlobalSectionNodeHandlerGetChildByIndex() throws ConfigurationException {
544         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
545         final SubnodeConfiguration sub = config.getSection(null);
546         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
547         final ImmutableNode rootNode = handler.getRootNode();
548         final ImmutableNode child = handler.getChild(rootNode, 0);
549         assertEquals("globalVar", child.getNodeName());
550         assertThrows(IndexOutOfBoundsException.class, () -> handler.getChild(rootNode, 1));
551     }
552 
553     /**
554      * Tests whether the node handler of a global section correctly filters named children.
555      */
556     @Test
557     public void testGlobalSectionNodeHandlerGetChildrenByName() throws ConfigurationException {
558         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
559         final SubnodeConfiguration sub = config.getSection(null);
560         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
561         assertTrue(handler.getChildren(sub.getModel().getNodeHandler().getRootNode(), "section1").isEmpty());
562     }
563 
564     /**
565      * Tests whether the node handler of a global section correctly determines the number of children.
566      */
567     @Test
568     public void testGlobalSectionNodeHandlerGetChildrenCount() throws ConfigurationException {
569         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
570         final SubnodeConfiguration sub = config.getSection(null);
571         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
572         assertEquals(1, handler.getChildrenCount(handler.getRootNode(), null));
573     }
574 
575     /**
576      * Tests whether the node handler of a global section correctly determines the index of a child.
577      */
578     @Test
579     public void testGlobalSectionNodeHandlerIndexOfChild() throws ConfigurationException {
580         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
581         final SubnodeConfiguration sub = config.getSection(null);
582         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
583         final List<ImmutableNode> children = handler.getRootNode().getChildren();
584         assertEquals(0, handler.indexOfChild(handler.getRootNode(), children.get(0)));
585         assertEquals(-1, handler.indexOfChild(handler.getRootNode(), children.get(1)));
586     }
587 
588     /**
589      * Test of isCommentLine method, of class {@link INIConfiguration}.
590      */
591     @Test
592     public void testIsCommentLine() {
593         final INIConfiguration instance = new INIConfiguration();
594         assertTrue(instance.isCommentLine("#comment1"));
595         assertTrue(instance.isCommentLine(";comment1"));
596         assertFalse(instance.isCommentLine("nocomment=true"));
597         assertFalse(instance.isCommentLine(null));
598     }
599 
600     /**
601      * Test of isSectionLine method, of class {@link INIConfiguration}.
602      */
603     @Test
604     public void testIsSectionLine() {
605         final INIConfiguration instance = new INIConfiguration();
606         assertTrue(instance.isSectionLine("[section]"));
607         assertFalse(instance.isSectionLine("nosection=true"));
608         assertFalse(instance.isSectionLine(null));
609     }
610 
611     /**
612      * Tests whether only properties with values occur in the enumeration of the global section.
613      */
614     @Test
615     public void testKeysOfGlobalSection() throws ConfigurationException {
616         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
617         final HierarchicalConfiguration<ImmutableNode> sub = config.getSection(null);
618         final Iterator<String> keys = sub.getKeys();
619         assertEquals("globalVar", keys.next());
620         if (keys.hasNext()) {
621             final StringBuilder buf = new StringBuilder();
622             do {
623                 buf.append(keys.next()).append(' ');
624             } while (keys.hasNext());
625             fail("Got additional keys: " + buf);
626         }
627     }
628 
629     /**
630      * Tests a property whose value spans multiple lines.
631      */
632     @Test
633     public void testLineContinuation() throws ConfigurationException {
634         final INIConfiguration config = setUpConfig(INI_DATA3);
635         assertEquals("one" + LINE_SEPARATOR + "two" + LINE_SEPARATOR + "three", config.getString("section5.multiLine"));
636     }
637 
638     /**
639      * Tests a line continuation at the end of the file.
640      */
641     @Test
642     public void testLineContinuationAtEnd() throws ConfigurationException {
643         final INIConfiguration config = setUpConfig(INI_DATA3);
644         assertEquals("one" + LINE_SEPARATOR, config.getString("section5.continueNoLine"));
645     }
646 
647     /**
648      * Tests a property whose value spans multiple lines with a comment.
649      */
650     @Test
651     public void testLineContinuationComment() throws ConfigurationException {
652         final INIConfiguration config = setUpConfig(INI_DATA3);
653         assertEquals("one" + LINE_SEPARATOR + "two", config.getString("section5.multiComment"));
654     }
655 
656     /**
657      * Tests a multi-line property value with an empty line.
658      */
659     @Test
660     public void testLineContinuationEmptyLine() throws ConfigurationException {
661         final INIConfiguration config = setUpConfig(INI_DATA3);
662         assertEquals(LINE_SEPARATOR + "line 2", config.getString("section5.noFirstLine"));
663     }
664 
665     /**
666      * Tests a property value that ends on a backslash, which is no line continuation character.
667      */
668     @Test
669     public void testLineContinuationNone() throws ConfigurationException {
670         final INIConfiguration config = setUpConfig(INI_DATA3);
671         assertEquals("C:\\Temp\\", config.getString("section5.singleLine"));
672     }
673 
674     /**
675      * Tests a property whose value spans multiple lines when quoting is involved. In this case whitespace must not be
676      * trimmed.
677      */
678     @Test
679     public void testLineContinuationQuoted() throws ConfigurationException {
680         final INIConfiguration config = setUpConfig(INI_DATA3);
681         assertEquals("one" + LINE_SEPARATOR + "  two  " + LINE_SEPARATOR + "three", config.getString("section5.multiQuoted"));
682     }
683 
684     /**
685      * Tests a property with a quoted value spanning multiple lines and a comment.
686      */
687     @Test
688     public void testLineContinuationQuotedComment() throws ConfigurationException {
689         final INIConfiguration config = setUpConfig(INI_DATA3);
690         assertEquals(" one " + LINE_SEPARATOR + "two", config.getString("section5.multiQuotedComment"));
691     }
692 
693     /**
694      * Tests whether the configuration deals correctly with list delimiters.
695      */
696     @Test
697     public void testListDelimiterHandling() throws ConfigurationException {
698         final INIConfiguration config = new INIConfiguration();
699         config.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
700         config.addProperty("list", "a,b,c");
701         config.addProperty("listesc", "3\\,1415");
702         final String output = saveToString(config);
703         final INIConfiguration config2 = setUpConfig(output);
704         assertEquals(Arrays.asList("a", "b", "c"), config2.getList("list"));
705         assertEquals("3,1415", config2.getString("listesc"));
706     }
707 
708     /**
709      * Tests whether property values are correctly escaped even if they are part of a property with multiple values.
710      */
711     @Test
712     public void testListDelimiterHandlingInList() throws ConfigurationException {
713         final String data = INI_DATA + "[sectest]" + LINE_SEPARATOR + "list = 3\\,1415,pi,\\\\Test\\,5" + LINE_SEPARATOR;
714         final INIConfiguration config = setUpConfig(data);
715         final INIConfiguration config2 = setUpConfig(saveToString(config));
716         final List<Object> list = config2.getList("sectest.list");
717         assertEquals(Arrays.asList("3,1415", "pi", "\\Test,5"), list);
718     }
719 
720     /**
721      * Tests whether parsing of lists can be disabled.
722      */
723     @Test
724     public void testListParsingDisabled() throws ConfigurationException {
725         final INIConfiguration config = new INIConfiguration();
726         load(config, "[test]" + LINE_SEPARATOR + "nolist=1,2,3");
727         assertEquals("1,2,3", config.getString("test.nolist"));
728     }
729 
730     /**
731      * Test of load method, of class {@link INIConfiguration}.
732      */
733     @Test
734     public void testLoad() throws Exception {
735         checkLoad(INI_DATA);
736     }
737 
738     /**
739      * Tests the load() method when the alternative value separator is used (a ':' for '=').
740      */
741     @Test
742     public void testLoadAlternativeSeparator() throws Exception {
743         checkLoad(INI_DATA.replace('=', ':'));
744     }
745 
746     /**
747      * Tests whether an instance can be created using a file-based builder.
748      */
749     @Test
750     public void testLoadFromBuilder() throws ConfigurationException, IOException {
751         final File file = writeTestFile(INI_DATA);
752         final FileBasedConfigurationBuilder<INIConfiguration> builder = new FileBasedConfigurationBuilder<>(INIConfiguration.class);
753         builder.configure(new FileBasedBuilderParametersImpl().setFile(file));
754         final INIConfiguration config = builder.getConfiguration();
755         checkContent(config);
756     }
757 
758     /**
759      * Tests whether a duplicate session is merged.
760      */
761     @Test
762     public void testMergeDuplicateSection() throws ConfigurationException, IOException {
763         final String data = "[section]\nvar1 = sec1\n\n" + "[section]\nvar2 = sec2\n";
764         final INIConfiguration config = setUpConfig(data);
765         assertEquals("sec1", config.getString("section.var1"));
766         assertEquals("sec2", config.getString("section.var2"));
767         final HierarchicalConfiguration<ImmutableNode> sub = config.getSection("section");
768         assertEquals("sec1", sub.getString("var1"));
769         assertEquals("sec2", sub.getString("var2"));
770         final StringWriter writer = new StringWriter();
771         config.write(writer);
772         final String content = writer.toString();
773         final int pos = content.indexOf("[section]");
774         assertTrue(pos >= 0);
775         assertTrue(content.indexOf("[section]", pos + 1) < 0);
776     }
777 
778     /**
779      * Tests property definitions containing multiple separators.
780      */
781     @Test
782     public void testMultipleSeparators() throws ConfigurationException {
783         final INIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
784         assertEquals("value=5", config.getString("section.var5"));
785         assertEquals("6=value", config.getString("section.var"));
786     }
787 
788     /**
789      * Tests property definitions containing multiple separators that are quoted.
790      */
791     @Test
792     public void testMultipleSeparatorsQuoted() throws ConfigurationException {
793         final INIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
794         assertEquals("value7", config.getString("section.var:7"));
795         assertEquals("value8", config.getString("section.var:8"));
796     }
797 
798     /**
799      * Tests that loading and saving a configuration that contains keys with delimiter characters works correctly. This test
800      * is related to CONFIGURATION-622.
801      */
802     @Test
803     public void testPropertyWithDelimiter() throws ConfigurationException {
804         final String data = INI_DATA + "key.dot = dotValue";
805         final INIConfiguration conf = new INIConfiguration();
806         load(conf, data);
807         assertEquals("dotValue", conf.getString("section3.key..dot"));
808         final String output = saveToString(conf);
809         assertThat(output, containsString("key.dot = dotValue"));
810     }
811 
812     @Test
813     public void testQuotedValue() throws Exception {
814         final INIConfiguration config = setUpConfig(INI_DATA2);
815         assertEquals("quoted value", config.getString("section4.var1"));
816     }
817 
818     /**
819      * Tests an empty quoted value.
820      */
821     @Test
822     public void testQuotedValueEmpty() throws ConfigurationException {
823         final INIConfiguration config = setUpConfig(INI_DATA2);
824         assertEquals("", config.getString("section4.var6"));
825     }
826 
827     @Test
828     public void testQuotedValueWithComment() throws Exception {
829         final INIConfiguration config = setUpConfig(INI_DATA2);
830         assertEquals("1;2;3", config.getString("section4.var4"));
831     }
832 
833     @Test
834     public void testQuotedValueWithQuotes() throws Exception {
835         final INIConfiguration config = setUpConfig(INI_DATA2);
836         assertEquals("quoted value\\nwith \"quotes\"", config.getString("section4.var2"));
837     }
838 
839     @Test
840     public void testQuotedValueWithSingleQuotes() throws Exception {
841         final INIConfiguration config = setUpConfig(INI_DATA2);
842         assertEquals("'quoted' \"value\"", config.getString("section4.var5"));
843     }
844 
845     /**
846      * Tests whether whitespace is left unchanged for quoted values.
847      */
848     @Test
849     public void testQuotedValueWithWhitespace() throws Exception {
850         final String content = "CmdPrompt = \" [test@cmd ~]$ \"";
851         final INIConfiguration config = setUpConfig(content);
852         assertEquals(" [test@cmd ~]$ ", config.getString("CmdPrompt"));
853     }
854 
855     /**
856      * Tests a quoted value with space and a comment.
857      */
858     @Test
859     public void testQuotedValueWithWhitespaceAndComment() throws Exception {
860         final String content = "CmdPrompt = \" [test@cmd ~]$ \" ; a comment";
861         final INIConfiguration config = setUpConfig(content);
862         assertEquals(" [test@cmd ~]$ ", config.getString("CmdPrompt"));
863     }
864 
865     /**
866      * Test of save method, of class {@link INIConfiguration}.
867      */
868     @Test
869     public void testSave() throws Exception {
870         final Writer writer = new StringWriter();
871         final INIConfiguration instance = new INIConfiguration();
872         instance.addProperty("section1.var1", "foo");
873         instance.addProperty("section1.var2", "451");
874         instance.addProperty("section2.var1", "123.45");
875         instance.addProperty("section2.var2", "bar");
876         instance.addProperty("section3.var1", "true");
877         instance.addProperty("section3.interpolated", "${section3.var1}");
878         instance.addProperty("section3.multi", "foo");
879         instance.addProperty("section3.multi", "bar");
880         instance.write(writer);
881 
882         assertEquals(INI_DATA, writer.toString());
883     }
884 
885     /**
886      * Tests whether a section that has been cleared can be manipulated and saved later.
887      */
888     @Test
889     public void testSaveClearedSection() throws ConfigurationException, IOException {
890         final String data = "[section]\ntest = failed\n";
891         final INIConfiguration config = setUpConfig(data);
892         SubnodeConfiguration sub = config.getSection("section");
893         assertFalse(sub.isEmpty());
894         sub.clear();
895         sub.close();
896         sub = config.getSection("section");
897         sub.setProperty("test", "success");
898         final StringWriter writer = new StringWriter();
899         config.write(writer);
900         final HierarchicalConfiguration<?> config2 = setUpConfig(writer.toString());
901         assertEquals("success", config2.getString("section.test"));
902     }
903 
904     /**
905      * Tests whether a configuration can be saved that contains section keys with delimiter characters. This test is related
906      * to CONFIGURATION-409.
907      */
908     @Test
909     public void testSaveKeysWithDelimiters() throws ConfigurationException, IOException {
910         INIConfiguration conf = new INIConfiguration();
911         final String section = "Section..with..dots";
912         conf.addProperty(section + ".test1", "test1");
913         conf.addProperty(section + ".test2", "test2");
914         final StringWriter writer = new StringWriter();
915         conf.write(writer);
916         conf = new INIConfiguration();
917         conf.read(new StringReader(writer.toString()));
918         assertEquals("test1", conf.getString(section + ".test1"));
919         assertEquals("test2", conf.getString(section + ".test2"));
920     }
921 
922     /**
923      * Tests whether list delimiter parsing can be disabled.
924      */
925     @Test
926     public void testSaveWithDelimiterParsingDisabled() throws ConfigurationException {
927         final INIConfiguration config = new INIConfiguration();
928         final String data = INI_DATA.substring(0, INI_DATA.length() - LINE_SEPARATOR.length()) + "nolist = 1,2, 3";
929         load(config, data);
930         assertEquals("1,2, 3", config.getString("section3.nolist"));
931         final String content = saveToString(config);
932         final INIConfiguration config2 = new INIConfiguration();
933         load(config2, content);
934         assertEquals("1,2, 3", config2.getString("section3.nolist"));
935     }
936 
937     /**
938      * Tests saving a configuration that contains a global section.
939      */
940     @Test
941     public void testSaveWithGlobalSection() throws ConfigurationException {
942         checkSave(INI_DATA_GLOBAL);
943     }
944 
945     /**
946      * Tests whether a configuration that contains only a global section can be saved correctly.
947      */
948     @Test
949     public void testSaveWithOnlyGlobalSection() throws ConfigurationException {
950         checkSave(INI_DATA_GLOBAL_ONLY);
951     }
952 
953     /**
954      * Tests whether the different separators with or without whitespace are recognized.
955      */
956     @Test
957     public void testSeparators() throws ConfigurationException {
958         final INIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
959         for (int i = 1; i <= 4; i++) {
960             assertEquals("value" + i, config.getString("section.var" + i));
961         }
962     }
963 
964     /**
965      * Test of read method with changed separator.
966      */
967     @Test
968     public void testSeparatorUsedInINIInput() throws Exception {
969         final String inputSeparator = "=";
970         final String input = "[section]" + LINE_SEPARATOR + "k1:v1$key1=value1" + LINE_SEPARATOR + "k1:v1,k2:v2$key2=value2" + LINE_SEPARATOR + "key3:value3"
971             + LINE_SEPARATOR + "key4 = value4" + LINE_SEPARATOR;
972 
973         final INIConfiguration instance = new FileBasedConfigurationBuilder<>(INIConfiguration.class)
974             .configure(new Parameters().ini().setSeparatorUsedInInput(inputSeparator)).getConfiguration();
975         load(instance, input);
976 
977         assertEquals("value1", instance.getString("section.k1:v1$key1"));
978         assertEquals("value2", instance.getString("section.k1:v1,k2:v2$key2"));
979         assertEquals("", instance.getString("section.key3:value3"));
980         assertEquals("value4", instance.getString("section.key4").trim());
981     }
982 
983     /**
984      * Test of save method with changed separator
985      */
986     @Test
987     public void testSeparatorUsedInINIOutput() throws Exception {
988         final String outputSeparator = ": ";
989         final String input = MessageFormat.format(INI_DATA4, "=").trim();
990         final String expectedOutput = MessageFormat.format(INI_DATA4, outputSeparator).trim();
991 
992         final INIConfiguration instance = new FileBasedConfigurationBuilder<>(INIConfiguration.class)
993             .configure(new Parameters().ini().setSeparatorUsedInOutput(outputSeparator)).getConfiguration();
994         load(instance, input);
995 
996         final Writer writer = new StringWriter();
997         instance.write(writer);
998         final String result = writer.toString().trim();
999 
1000         assertEquals(expectedOutput, result);
1001     }
1002 
1003     /**
1004      * Test correct handling of in line comments on value line
1005      */
1006     @ParameterizedTest
1007     @MethodSource("provideValuesWithComments")
1008     public void testValueWithComment(final String source, final String key, final String value) throws Exception {
1009         final INIConfiguration config = setUpConfig(source);
1010         assertEquals(value, config.getString(key));
1011     }
1012 
1013     private static Stream<Arguments> provideValuesWithComments() {
1014         return Stream.of(
1015                 Arguments.of(INI_DATA2, "section4.var3", "123"),
1016                 Arguments.of(INI_DATA2, "section4.var4", "1;2;3"),
1017                 Arguments.of(INI_DATA2, "section4.var5", "'quoted' \"value\""),
1018                 Arguments.of(INI_DATA5, "section4.var3", "123"),
1019                 Arguments.of(INI_DATA5, "section4.var4", "1#2;3"),
1020                 Arguments.of(INI_DATA5, "section4.var5", "'quoted' \"value\"")
1021         );
1022     }
1023 
1024     /**
1025      * Tests whether the list delimiter character is recognized.
1026      */
1027     @Test
1028     public void testValueWithDelimiters() throws ConfigurationException {
1029         final INIConfiguration config = setUpConfig("[test]" + LINE_SEPARATOR + "list=1,2,3" + LINE_SEPARATOR);
1030         final List<Object> list = config.getList("test.list");
1031         assertEquals(Arrays.asList("1", "2", "3"), list);
1032     }
1033 
1034     /**
1035      * Tests whether a value which contains a semicolon can be loaded successfully. This test is related to
1036      * CONFIGURATION-434.
1037      */
1038     @Test
1039     public void testValueWithSemicolon() throws ConfigurationException {
1040         final String path = "C:\\Program Files\\jar\\manage.jar;" + "C:\\Program Files\\jar\\guiLauncher.jar";
1041         final String content = "[Environment]" + LINE_SEPARATOR + "Application Type=any" + LINE_SEPARATOR + "Class Path=" + path + "  ;comment" + LINE_SEPARATOR
1042             + "Path=" + path + "\t; another comment";
1043         final INIConfiguration config = setUpConfig(content);
1044         assertEquals(path, config.getString("Environment.Class Path"));
1045         assertEquals(path, config.getString("Environment.Path"));
1046     }
1047 
1048     /**
1049      * Tests whether an empty section can be saved. This is related to CONFIGURATION-671.
1050      */
1051     @Test
1052     public void testWriteEmptySection() throws ConfigurationException, IOException {
1053         final String section = "[EmptySection]";
1054         final INIConfiguration config = setUpConfig(section);
1055         assertEquals(Collections.singleton("EmptySection"), config.getSections());
1056 
1057         final StringWriter writer = new StringWriter();
1058         config.write(writer);
1059         assertEquals(section + LINE_SEPARATOR + LINE_SEPARATOR, writer.toString());
1060     }
1061 
1062     @Test
1063     public void testWriteValueWithCommentChar() throws Exception {
1064         final INIConfiguration config = new INIConfiguration();
1065         config.setProperty("section.key1", "1;2;3");
1066 
1067         final StringWriter writer = new StringWriter();
1068         config.write(writer);
1069 
1070         final INIConfiguration config2 = new INIConfiguration();
1071         config2.read(new StringReader(writer.toString()));
1072 
1073         assertEquals("1;2;3", config2.getString("section.key1"));
1074     }
1075 
1076     /**
1077      * Tests whether a section with inline comment is correctly parsed.
1078      */
1079     @ParameterizedTest
1080     @MethodSource("provideSectionsWithComments")
1081     public void testGetSectionsWithInLineComment(final String source, final boolean allowComments, final String[] results) throws ConfigurationException {
1082         final INIConfiguration config = setUpConfig(source, allowComments);
1083         checkSectionNames(config, results);
1084     }
1085 
1086     private static Stream<Arguments> provideSectionsWithComments() {
1087         return Stream.of(
1088                 Arguments.of(INI_DATA6, false, new String[]{null, "section11] ; sub-section related to [section1"}),
1089                 Arguments.of(INI_DATA7, false, new String[]{null, "section11] # sub-section related to [section1"}),
1090                 Arguments.of(INI_DATA6, true, new String[]{"section1", "section11"}),
1091                 Arguments.of(INI_DATA7, true, new String[]{"section1", "section11"})
1092         );
1093     }
1094 
1095     /**
1096      * Writes a test ini file.
1097      *
1098      * @param content the content of the file
1099      * @return the newly created file
1100      * @throws IOException if an error occurs
1101      */
1102     private File writeTestFile(final String content) throws IOException {
1103         final File file = newFile(tempFolder);
1104         try (PrintWriter out = new PrintWriter(new FileWriter(file))) {
1105             out.println(content);
1106         }
1107         return file;
1108     }
1109 }