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