View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.PrintWriter;
22  import java.io.Reader;
23  import java.io.Writer;
24  import java.util.LinkedHashMap;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  
31  import org.apache.commons.configuration2.convert.ListDelimiterHandler;
32  import org.apache.commons.configuration2.ex.ConfigurationException;
33  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
34  import org.apache.commons.configuration2.tree.ImmutableNode;
35  import org.apache.commons.configuration2.tree.InMemoryNodeModel;
36  import org.apache.commons.configuration2.tree.InMemoryNodeModelSupport;
37  import org.apache.commons.configuration2.tree.NodeHandler;
38  import org.apache.commons.configuration2.tree.NodeHandlerDecorator;
39  import org.apache.commons.configuration2.tree.NodeSelector;
40  import org.apache.commons.configuration2.tree.TrackedNodeModel;
41  
42  /**
43   * <p>
44   * A specialized hierarchical configuration implementation for parsing ini files.
45   * </p>
46   * <p>
47   * An initialization or ini file is a configuration file typically found on Microsoft's Windows operating system and
48   * contains data for Windows based applications.
49   * </p>
50   * <p>
51   * Although popularized by Windows, ini files can be used on any system or platform due to the fact that they are merely
52   * text files that can easily be parsed and modified by both humans and computers.
53   * </p>
54   * <p>
55   * A typical ini file could look something like:
56   * </p>
57   *
58   * <pre>
59   * [section1]
60   * ; this is a comment!
61   * var1 = foo
62   * var2 = bar
63   *
64   * [section2]
65   * var1 = doo
66   * </pre>
67   * <p>
68   * The format of ini files is fairly straight forward and is composed of three components:
69   * </p>
70   * <ul>
71   * <li><b>Sections:</b> Ini files are split into sections, each section starting with a section declaration. A section
72   * declaration starts with a '[' and ends with a ']'. Sections occur on one line only.</li>
73   * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters have a typical {@code key = value}
74   * format.</li>
75   * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
76   * </ul>
77   * <p>
78   * There are various implementations of the ini file format by various vendors which has caused a number of differences
79   * to appear. As far as possible this configuration tries to be lenient and support most of the differences.
80   * </p>
81   * <p>
82   * Some of the differences supported are as follows:
83   * </p>
84   * <ul>
85   * <li><b>Comments:</b> The '#' character is also accepted as a comment signifier.</li>
86   * <li><b>Key value separator:</b> The ':' character is also accepted in place of '=' to separate keys and values in
87   * parameters, for example {@code var1 : foo}.</li>
88   * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed, this configuration does however support
89   * this feature. In the event of a duplicate section, the two section's values are merged so that there is only a single
90   * section. <strong>Note</strong>: This also affects the internal data of the configuration. If it is saved, only a
91   * single section is written!</li>
92   * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only allowed if they are in two different
93   * sections, thus they are local to sections; this configuration simply merges duplicates; if a section has a duplicate
94   * parameter the values are then added to the key as a list.</li>
95   * </ul>
96   * <p>
97   * Global parameters are also allowed; any parameters declared before a section is declared are added to a global
98   * section. It is important to note that this global section does not have a name.
99   * </p>
100  * <p>
101  * In all instances, a parameter's key is prepended with its section name and a '.' (period). Thus a parameter named
102  * "var1" in "section1" will have the key {@code section1.var1} in this configuration. (This is the default behavior.
103  * Because this is a hierarchical configuration you can change this by setting a different
104  * {@link org.apache.commons.configuration2.tree.ExpressionEngine}.)
105  * </p>
106  * <p>
107  * <strong>Implementation Details:</strong> Consider the following ini file:
108  * </p>
109  * <pre>
110  *  default = ok
111  *
112  *  [section1]
113  *  var1 = foo
114  *  var2 = doodle
115  *
116  *  [section2]
117  *  ; a comment
118  *  var1 = baz
119  *  var2 = shoodle
120  *  bad =
121  *  = worse
122  *
123  *  [section3]
124  *  # another comment
125  *  var1 : foo
126  *  var2 : bar
127  *  var5 : test1
128  *
129  *  [section3]
130  *  var3 = foo
131  *  var4 = bar
132  *  var5 = test2
133  *
134  *  [sectionSeparators]
135  *  passwd : abc=def
136  *  a:b = "value"
137  * </pre>
138  * <p>
139  * This ini file will be parsed without error. Note:
140  * </p>
141  * <ul>
142  * <li>The parameter named "default" is added to the global section, it's value is accessed simply using
143  * {@code getProperty("default")}.</li>
144  * <li>Section 1's parameters can be accessed using {@code getProperty("section1.var1")}.</li>
145  * <li>The parameter named "bad" simply adds the parameter with an empty value.</li>
146  * <li>The empty key with value "= worse" is added using a key consisting of a single space character. This key is still
147  * added to section 2 and the value can be accessed using {@code getProperty("section2. ")}, notice the period '.' and
148  * the space following the section name.</li>
149  * <li>Section three uses both '=' and ':' to separate keys and values.</li>
150  * <li>Section 3 has a duplicate key named "var5". The value for this key is [test1, test2], and is represented as a
151  * List.</li>
152  * <li>The section called <em>sectionSeparators</em> demonstrates how the configuration deals with multiple occurrences
153  * of separator characters. Per default the first separator character in a line is detected and used to split the key
154  * from the value. Therefore the first property definition in this section has the key {@code passwd} and the value
155  * {@code abc=def}. This default behavior can be changed by using quotes. If there is a separator character before the
156  * first quote character (ignoring whitespace), this character is used as separator. Thus the second property definition
157  * in the section has the key {@code a:b} and the value {@code value}.</li>
158  * </ul>
159  * <p>
160  * Internally, this configuration maps the content of the represented ini file to its node structure in the following
161  * way:
162  * </p>
163  * <ul>
164  * <li>Sections are represented by direct child nodes of the root node.</li>
165  * <li>For the content of a section, corresponding nodes are created as children of the section node.</li>
166  * </ul>
167  * <p>
168  * This explains how the keys for the properties can be constructed. You can also use other methods of
169  * {@link HierarchicalConfiguration} for querying or manipulating the hierarchy of configuration nodes, for instance the
170  * {@code configurationAt()} method for obtaining the data of a specific section. However, be careful that the storage
171  * scheme described above is not violated (e.g. by adding multiple levels of nodes or inserting duplicate section
172  * nodes). Otherwise, the special methods for ini configurations may not work correctly!
173  * </p>
174  * <p>
175  * The set of sections in this configuration can be retrieved using the {@code getSections()} method. For obtaining a
176  * {@code SubnodeConfiguration} with the content of a specific section the {@code getSection()} method can be used.
177  * </p>
178  * <p>
179  * Like other {@code Configuration} implementations, this class uses a {@code Synchronizer} object to control concurrent
180  * access. By choosing a suitable implementation of the {@code Synchronizer} interface, an instance can be made
181  * thread-safe or not. Note that access to most of the properties typically set through a builder is not protected by
182  * the {@code Synchronizer}. The intended usage is that these properties are set once at construction time through the
183  * builder and after that remain constant. If you wish to change such properties during life time of an instance, you
184  * have to use the {@code lock()} and {@code unlock()} methods manually to ensure that other threads see your changes.
185  * </p>
186  * <p>
187  * As this class extends {@link AbstractConfiguration}, all basic features like variable interpolation, list handling,
188  * or data type conversions are available as well. This is described in the chapter
189  * <a href="https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html"> Basic features
190  * and AbstractConfiguration</a> of the user's guide.
191  * </p>
192  * <p>
193  * Note that this configuration does not support properties with null values. Such properties are considered to be
194  * section nodes.
195  * </p>
196  *
197  * @since 1.6
198  */
199 public class INIConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration {
200 
201     /**
202      * The default characters that signal the start of a comment line.
203      */
204     protected static final String COMMENT_CHARS = "#;";
205 
206     /**
207      * The default characters used to separate keys from values.
208      */
209     protected static final String SEPARATOR_CHARS = "=:";
210 
211     /**
212      * Constant for the line separator.
213      */
214     private static final String LINE_SEPARATOR = System.lineSeparator();
215 
216     /**
217      * The characters used for quoting values.
218      */
219     private static final String QUOTE_CHARACTERS = "\"'";
220 
221     /**
222      * The line continuation character.
223      */
224     private static final String LINE_CONT = "\\";
225 
226     /**
227      * The separator used when writing an INI file.
228      */
229     private String separatorUsedInOutput = " = ";
230 
231     /**
232      * The separator used when reading an INI file.
233      */
234     private String separatorUsedInInput = SEPARATOR_CHARS;
235 
236     /**
237      * The characters used to separate keys from values when reading an INI file.
238      */
239     private String commentCharsUsedInInput = COMMENT_CHARS;
240 
241     /**
242      * The flag for decision, whether inline comments on the section line are allowed.
243      */
244     private boolean sectionInLineCommentsAllowed;
245 
246     /**
247      * Create a new empty INI Configuration.
248      */
249     public INIConfiguration() {
250     }
251 
252     /**
253      * Creates a new instance of {@code INIConfiguration} with the content of the specified
254      * {@code HierarchicalConfiguration}.
255      *
256      * @param c the configuration to be copied
257      * @since 2.0
258      */
259     public INIConfiguration(final HierarchicalConfiguration<ImmutableNode> c) {
260         super(c);
261     }
262 
263     /**
264      * Create a new empty INI Configuration with option to allow inline comments on the section line.
265      *
266      * @param sectionInLineCommentsAllowed when true inline comments on the section line are allowed
267      */
268     private INIConfiguration(final boolean sectionInLineCommentsAllowed) {
269         this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed;
270     }
271 
272     /**
273      * Creates a new builder.
274      *
275      * @return a new builder.
276      * @since 2.9.0
277      */
278     public static Builder builder() {
279         return new Builder();
280     }
281 
282     /**
283      * Builds instances of INIConfiguration.
284      *
285      * @since 2.9.0
286      */
287     public static class Builder {
288 
289         /**
290          * The flag for decision, whether inline comments on the section line are allowed.
291          */
292         private boolean sectionInLineCommentsAllowed;
293 
294         public Builder setSectionInLineCommentsAllowed(final boolean sectionInLineCommentsAllowed) {
295             this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed;
296             return this;
297         }
298 
299         public INIConfiguration build() {
300             return new INIConfiguration(sectionInLineCommentsAllowed);
301         }
302 
303     }
304 
305     /**
306      * Gets separator used in INI output. see {@code setSeparatorUsedInOutput} for further explanation
307      *
308      * @return the current separator for writing the INI output
309      * @since 2.2
310      */
311     public String getSeparatorUsedInOutput() {
312         beginRead(false);
313         try {
314             return separatorUsedInOutput;
315         } finally {
316             endRead();
317         }
318     }
319 
320     /**
321      * Allows setting the key and value separator which is used for the creation of the resulting INI output
322      *
323      * @param separator String of the new separator for INI output
324      * @since 2.2
325      */
326     public void setSeparatorUsedInOutput(final String separator) {
327         beginWrite(false);
328         try {
329             this.separatorUsedInOutput = separator;
330         } finally {
331             endWrite();
332         }
333     }
334 
335     /**
336      * Gets separator used in INI reading. see {@code setSeparatorUsedInInput} for further explanation
337      *
338      * @return the current separator for reading the INI input
339      * @since 2.5
340      */
341     public String getSeparatorUsedInInput() {
342         beginRead(false);
343         try {
344             return separatorUsedInInput;
345         } finally {
346             endRead();
347         }
348     }
349 
350     /**
351      * Allows setting the key and value separator which is used in reading an INI file
352      *
353      * @param separator String of the new separator for INI reading
354      * @since 2.5
355      */
356     public void setSeparatorUsedInInput(final String separator) {
357         beginRead(false);
358         try {
359             this.separatorUsedInInput = separator;
360         } finally {
361             endRead();
362         }
363     }
364 
365     /**
366      * Gets comment leading separator used in INI reading. see {@code setCommentLeadingCharsUsedInInput} for further
367      * explanation
368      *
369      * @return the current separator for reading the INI input
370      * @since 2.5
371      */
372     public String getCommentLeadingCharsUsedInInput() {
373         beginRead(false);
374         try {
375             return commentCharsUsedInInput;
376         } finally {
377             endRead();
378         }
379     }
380 
381     /**
382      * Allows setting the leading comment separator which is used in reading an INI file
383      *
384      * @param separator String of the new separator for INI reading
385      * @since 2.5
386      */
387     public void setCommentLeadingCharsUsedInInput(final String separator) {
388         beginRead(false);
389         try {
390             this.commentCharsUsedInInput = separator;
391         } finally {
392             endRead();
393         }
394     }
395 
396     /**
397      * Save the configuration to the specified writer.
398      *
399      * @param writer - The writer to save the configuration to.
400      * @throws ConfigurationException If an error occurs while writing the configuration
401      * @throws IOException if an I/O error occurs.
402      */
403     @Override
404     public void write(final Writer writer) throws ConfigurationException, IOException {
405         final PrintWriter out = new PrintWriter(writer);
406         boolean first = true;
407         final String separator = getSeparatorUsedInOutput();
408 
409         beginRead(false);
410         try {
411             for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) {
412                 if (isSectionNode(node)) {
413                     if (!first) {
414                         out.println();
415                     }
416                     out.print("[");
417                     out.print(node.getNodeName());
418                     out.print("]");
419                     out.println();
420 
421                     node.forEach(child -> writeProperty(out, child.getNodeName(), child.getValue(), separator));
422                 } else {
423                     writeProperty(out, node.getNodeName(), node.getValue(), separator);
424                 }
425                 first = false;
426             }
427             out.println();
428             out.flush();
429         } finally {
430             endRead();
431         }
432     }
433 
434     /**
435      * Load the configuration from the given reader. Note that the {@code clear()} method is not called so the configuration
436      * read in will be merged with the current configuration.
437      *
438      * @param in the reader to read the configuration from.
439      * @throws ConfigurationException If an error occurs while reading the configuration
440      * @throws IOException if an I/O error occurs.
441      */
442     @Override
443     public void read(final Reader in) throws ConfigurationException, IOException {
444         final BufferedReader bufferedReader = new BufferedReader(in);
445         final Map<String, ImmutableNode.Builder> sectionBuilders = new LinkedHashMap<>();
446         final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
447 
448         createNodeBuilders(bufferedReader, rootBuilder, sectionBuilders);
449         final ImmutableNode rootNode = createNewRootNode(rootBuilder, sectionBuilders);
450         addNodes(null, rootNode.getChildren());
451     }
452 
453     /**
454      * Creates a new root node from the builders constructed while reading the configuration file.
455      *
456      * @param rootBuilder the builder for the top-level section
457      * @param sectionBuilders a map storing the section builders
458      * @return the root node of the newly created hierarchy
459      */
460     private static ImmutableNode createNewRootNode(final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders) {
461         sectionBuilders.forEach((k, v) -> rootBuilder.addChild(v.name(k).create()));
462         return rootBuilder.create();
463     }
464 
465     /**
466      * Reads the content of an INI file from the passed in reader and creates a structure of builders for constructing the
467      * {@code ImmutableNode} objects representing the data.
468      *
469      * @param in the reader
470      * @param rootBuilder the builder for the top-level section
471      * @param sectionBuilders a map storing the section builders
472      * @throws IOException if an I/O error occurs.
473      */
474     private void createNodeBuilders(final BufferedReader in, final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders)
475         throws IOException {
476         ImmutableNode.Builder sectionBuilder = rootBuilder;
477         String line = in.readLine();
478         while (line != null) {
479             line = line.trim();
480             if (!isCommentLine(line)) {
481                 if (isSectionLine(line)) {
482                     final int length = sectionInLineCommentsAllowed ? line.indexOf("]") : line.length() - 1;
483                     final String section = line.substring(1, length);
484                     sectionBuilder = sectionBuilders.computeIfAbsent(section, k -> new ImmutableNode.Builder());
485                 } else {
486                     String key;
487                     String value = "";
488                     final int index = findSeparator(line);
489                     if (index >= 0) {
490                         key = line.substring(0, index);
491                         value = parseValue(line.substring(index + 1), in);
492                     } else {
493                         key = line;
494                     }
495                     key = key.trim();
496                     if (key.isEmpty()) {
497                         // use space for properties with no key
498                         key = " ";
499                     }
500                     createValueNodes(sectionBuilder, key, value);
501                 }
502             }
503 
504             line = in.readLine();
505         }
506     }
507 
508     /**
509      * Creates the node(s) for the given key value-pair. If delimiter parsing is enabled, the value string is split if
510      * possible, and for each single value a node is created. Otherwise only a single node is added to the section.
511      *
512      * @param sectionBuilder the section builder for adding new nodes
513      * @param key the key
514      * @param value the value string
515      */
516     private void createValueNodes(final ImmutableNode.Builder sectionBuilder, final String key, final String value) {
517         getListDelimiterHandler().split(value, false).forEach(v -> sectionBuilder.addChild(new ImmutableNode.Builder().name(key).value(v).create()));
518     }
519 
520     /**
521      * Writes data about a property into the given stream.
522      *
523      * @param out the output stream
524      * @param key the key
525      * @param value the value
526      */
527     private void writeProperty(final PrintWriter out, final String key, final Object value, final String separator) {
528         out.print(key);
529         out.print(separator);
530         out.print(escapeValue(value.toString()));
531         out.println();
532     }
533 
534     /**
535      * Parse the value to remove the quotes and ignoring the comment. Example:
536      *
537      * <pre>
538      * &quot;value&quot; ; comment -&gt; value
539      * </pre>
540      *
541      * <pre>
542      * 'value' ; comment -&gt; value
543      * </pre>
544      *
545      * Note that a comment character is only recognized if there is at least one whitespace character before it. So it can
546      * appear in the property value, e.g.:
547      *
548      * <pre>
549      * C:\\Windows;C:\\Windows\\system32
550      * </pre>
551      *
552      * @param val the value to be parsed
553      * @param reader the reader (needed if multiple lines have to be read)
554      * @throws IOException if an IO error occurs
555      */
556     private String parseValue(final String val, final BufferedReader reader) throws IOException {
557         final StringBuilder propertyValue = new StringBuilder();
558         boolean lineContinues;
559         String value = val.trim();
560 
561         do {
562             final boolean quoted = value.startsWith("\"") || value.startsWith("'");
563             boolean stop = false;
564             boolean escape = false;
565 
566             final char quote = quoted ? value.charAt(0) : 0;
567 
568             int i = quoted ? 1 : 0;
569 
570             final StringBuilder result = new StringBuilder();
571             char lastChar = 0;
572             while (i < value.length() && !stop) {
573                 final char c = value.charAt(i);
574 
575                 if (quoted) {
576                     if ('\\' == c && !escape) {
577                         escape = true;
578                     } else if (!escape && quote == c) {
579                         stop = true;
580                     } else {
581                         if (escape && quote == c) {
582                             escape = false;
583                         } else if (escape) {
584                             escape = false;
585                             result.append('\\');
586                         }
587                         result.append(c);
588                     }
589                 } else if (isCommentChar(c) && Character.isWhitespace(lastChar)) {
590                     stop = true;
591                 } else {
592                     result.append(c);
593                 }
594 
595                 i++;
596                 lastChar = c;
597             }
598 
599             String v = result.toString();
600             if (!quoted) {
601                 v = v.trim();
602                 lineContinues = lineContinues(v);
603                 if (lineContinues) {
604                     // remove trailing "\"
605                     v = v.substring(0, v.length() - 1).trim();
606                 }
607             } else {
608                 lineContinues = lineContinues(value, i);
609             }
610             propertyValue.append(v);
611 
612             if (lineContinues) {
613                 propertyValue.append(LINE_SEPARATOR);
614                 value = reader.readLine();
615             }
616         } while (lineContinues && value != null);
617 
618         return propertyValue.toString();
619     }
620 
621     /**
622      * Tests whether the specified string contains a line continuation marker.
623      *
624      * @param line the string to check
625      * @return a flag whether this line continues
626      */
627     private static boolean lineContinues(final String line) {
628         final String s = line.trim();
629         return s.equals(LINE_CONT) || s.length() > 2 && s.endsWith(LINE_CONT) && Character.isWhitespace(s.charAt(s.length() - 2));
630     }
631 
632     /**
633      * Tests whether the specified string contains a line continuation marker after the specified position. This method
634      * parses the string to remove a comment that might be present. Then it checks whether a line continuation marker can be
635      * found at the end.
636      *
637      * @param line the line to check
638      * @param pos the start position
639      * @return a flag whether this line continues
640      */
641     private boolean lineContinues(final String line, final int pos) {
642         final String s;
643 
644         if (pos >= line.length()) {
645             s = line;
646         } else {
647             int end = pos;
648             while (end < line.length() && !isCommentChar(line.charAt(end))) {
649                 end++;
650             }
651             s = line.substring(pos, end);
652         }
653 
654         return lineContinues(s);
655     }
656 
657     /**
658      * Tests whether the specified character is a comment character.
659      *
660      * @param c the character
661      * @return a flag whether this character starts a comment
662      */
663     private boolean isCommentChar(final char c) {
664         return getCommentLeadingCharsUsedInInput().indexOf(c) >= 0;
665     }
666 
667     /**
668      * Tries to find the index of the separator character in the given string. This method checks for the presence of
669      * separator characters in the given string. If multiple characters are found, the first one is assumed to be the
670      * correct separator. If there are quoting characters, they are taken into account, too.
671      *
672      * @param line the line to be checked
673      * @return the index of the separator character or -1 if none is found
674      */
675     private int findSeparator(final String line) {
676         int index = findSeparatorBeforeQuote(line, findFirstOccurrence(line, QUOTE_CHARACTERS));
677         if (index < 0) {
678             index = findFirstOccurrence(line, getSeparatorUsedInInput());
679         }
680         return index;
681     }
682 
683     /**
684      * Checks for the occurrence of the specified separators in the given line. The index of the first separator is
685      * returned.
686      *
687      * @param line the line to be investigated
688      * @param separators a string with the separator characters to look for
689      * @return the lowest index of a separator character or -1 if no separator is found
690      */
691     private static int findFirstOccurrence(final String line, final String separators) {
692         int index = -1;
693 
694         for (int i = 0; i < separators.length(); i++) {
695             final char sep = separators.charAt(i);
696             final int pos = line.indexOf(sep);
697             if (pos >= 0 && (index < 0 || pos < index)) {
698                 index = pos;
699             }
700         }
701 
702         return index;
703     }
704 
705     /**
706      * Searches for a separator character directly before a quoting character. If the first non-whitespace character before
707      * a quote character is a separator, it is considered the "real" separator in this line - even if there are other
708      * separators before.
709      *
710      * @param line the line to be investigated
711      * @param quoteIndex the index of the quote character
712      * @return the index of the separator before the quote or &lt; 0 if there is none
713      */
714     private static int findSeparatorBeforeQuote(final String line, final int quoteIndex) {
715         int index = quoteIndex - 1;
716         while (index >= 0 && Character.isWhitespace(line.charAt(index))) {
717             index--;
718         }
719 
720         if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0) {
721             index = -1;
722         }
723 
724         return index;
725     }
726 
727     /**
728      * Escapes the given property value before it is written. This method add quotes around the specified value if it
729      * contains a comment character and handles list delimiter characters.
730      *
731      * @param value the string to be escaped
732      */
733     private String escapeValue(final String value) {
734         return String.valueOf(getListDelimiterHandler().escape(escapeComments(value), ListDelimiterHandler.NOOP_TRANSFORMER));
735     }
736 
737     /**
738      * Escapes comment characters in the given value.
739      *
740      * @param value the value to be escaped
741      * @return the value with comment characters escaped
742      */
743     private String escapeComments(final String value) {
744         final String commentChars = getCommentLeadingCharsUsedInInput();
745         boolean quoted = false;
746 
747         for (int i = 0; i < commentChars.length(); i++) {
748             final char c = commentChars.charAt(i);
749             if (value.indexOf(c) != -1) {
750                 quoted = true;
751                 break;
752             }
753         }
754 
755         if (quoted) {
756             return '"' + value.replace("\"", "\\\"") + '"';
757         }
758         return value;
759     }
760 
761     /**
762      * Determine if the given line is a comment line.
763      *
764      * @param line The line to check.
765      * @return true if the line is empty or starts with one of the comment characters
766      */
767     protected boolean isCommentLine(final String line) {
768         if (line == null) {
769             return false;
770         }
771         // blank lines are also treated as comment lines
772         return line.isEmpty() || getCommentLeadingCharsUsedInInput().indexOf(line.charAt(0)) >= 0;
773     }
774 
775     /**
776      * Determine if the given line is a section.
777      *
778      * @param line The line to check.
779      * @return true if the line contains a section
780      */
781     protected boolean isSectionLine(final String line) {
782         if (line == null) {
783             return false;
784         }
785         return sectionInLineCommentsAllowed ? isNonStrictSection(line) : isStrictSection(line);
786     }
787 
788     /**
789      * Determine if the entire given line is a section - inline comments are not allowed.
790      *
791      * @param line The line to check.
792      * @return true if the entire line is a section
793      */
794     private static boolean isStrictSection(final String line) {
795         return line.startsWith("[") && line.endsWith("]");
796     }
797 
798     /**
799      * Determine if the given line contains a section - inline comments are allowed.
800      *
801      * @param line The line to check.
802      * @return true if the line contains a section
803      */
804     private static boolean isNonStrictSection(final String line) {
805         return line.startsWith("[") && line.contains("]");
806     }
807 
808     /**
809      * Gets a set containing the sections in this ini configuration. Note that changes to this set do not affect the
810      * configuration.
811      *
812      * @return a set containing the sections.
813      */
814     public Set<String> getSections() {
815         final Set<String> sections = new LinkedHashSet<>();
816         boolean globalSection = false;
817         boolean inSection = false;
818 
819         beginRead(false);
820         try {
821             for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) {
822                 if (isSectionNode(node)) {
823                     inSection = true;
824                     sections.add(node.getNodeName());
825                 } else if (!inSection && !globalSection) {
826                     globalSection = true;
827                     sections.add(null);
828                 }
829             }
830         } finally {
831             endRead();
832         }
833 
834         return sections;
835     }
836 
837     /**
838      * Gets a configuration with the content of the specified section. This provides an easy way of working with a single
839      * section only. The way this configuration is structured internally, this method is very similar to calling
840      * {@link HierarchicalConfiguration#configurationAt(String)} with the name of the section in question. There are the
841      * following differences however:
842      * <ul>
843      * <li>This method never throws an exception. If the section does not exist, it is created now. The configuration
844      * returned in this case is empty.</li>
845      * <li>If section is contained multiple times in the configuration, the configuration returned by this method is
846      * initialized with the first occurrence of the section. (This can only happen if {@code addProperty()} has been used in
847      * a way that does not conform to the storage scheme used by {@code INIConfiguration}. If used correctly, there will not
848      * be duplicate sections.)</li>
849      * <li>There is special support for the global section: Passing in <b>null</b> as section name returns a configuration
850      * with the content of the global section (which may also be empty).</li>
851      * </ul>
852      *
853      * @param name the name of the section in question; <b>null</b> represents the global section
854      * @return a configuration containing only the properties of the specified section
855      */
856     public SubnodeConfiguration getSection(final String name) {
857         if (name == null) {
858             return getGlobalSection();
859         }
860         try {
861             return (SubnodeConfiguration) configurationAt(name, true);
862         } catch (final ConfigurationRuntimeException iex) {
863             // the passed in key does not map to exactly one node
864             // obtain the node for the section, create it on demand
865             final InMemoryNodeModel parentModel = getSubConfigurationParentModel();
866             final NodeSelector selector = parentModel.trackChildNodeWithCreation(null, name, this);
867             return createSubConfigurationForTrackedNode(selector, this);
868         }
869     }
870 
871     /**
872      * Creates a sub configuration for the global section of the represented INI configuration.
873      *
874      * @return the sub configuration for the global section
875      */
876     private SubnodeConfiguration getGlobalSection() {
877         final InMemoryNodeModel parentModel = getSubConfigurationParentModel();
878         final NodeSelector selector = new NodeSelector(null); // selects parent
879         parentModel.trackNode(selector, this);
880         final GlobalSectionNodeModel model = new GlobalSectionNodeModel(this, selector);
881         final SubnodeConfiguration sub = new SubnodeConfiguration(this, model);
882         initSubConfigurationForThisParent(sub);
883         return sub;
884     }
885 
886     /**
887      * Checks whether the specified configuration node represents a section.
888      *
889      * @param node the node in question
890      * @return a flag whether this node represents a section
891      */
892     private static boolean isSectionNode(final ImmutableNode node) {
893         return node.getValue() == null;
894     }
895 
896     /**
897      * A specialized node model implementation for the sub configuration representing the global section of the INI file.
898      * This is a regular {@code TrackedNodeModel} with one exception: The {@code NodeHandler} used by this model applies a
899      * filter on the children of the root node so that only nodes are visible that are no sub sections.
900      */
901     private static final class GlobalSectionNodeModel extends TrackedNodeModel {
902         /**
903          * Creates a new instance of {@code GlobalSectionNodeModel} and initializes it with the given underlying model.
904          *
905          * @param modelSupport the underlying {@code InMemoryNodeModel}
906          * @param selector the {@code NodeSelector}
907          */
908         public GlobalSectionNodeModel(final InMemoryNodeModelSupport modelSupport, final NodeSelector selector) {
909             super(modelSupport, selector, true);
910         }
911 
912         @Override
913         public NodeHandler<ImmutableNode> getNodeHandler() {
914             return new NodeHandlerDecorator<ImmutableNode>() {
915                 @Override
916                 public List<ImmutableNode> getChildren(final ImmutableNode node) {
917                     final List<ImmutableNode> children = super.getChildren(node);
918                     return filterChildrenOfGlobalSection(node, children);
919                 }
920 
921                 @Override
922                 public List<ImmutableNode> getChildren(final ImmutableNode node, final String name) {
923                     final List<ImmutableNode> children = super.getChildren(node, name);
924                     return filterChildrenOfGlobalSection(node, children);
925                 }
926 
927                 @Override
928                 public int getChildrenCount(final ImmutableNode node, final String name) {
929                     final List<ImmutableNode> children = name != null ? super.getChildren(node, name) : super.getChildren(node);
930                     return filterChildrenOfGlobalSection(node, children).size();
931                 }
932 
933                 @Override
934                 public ImmutableNode getChild(final ImmutableNode node, final int index) {
935                     final List<ImmutableNode> children = super.getChildren(node);
936                     return filterChildrenOfGlobalSection(node, children).get(index);
937                 }
938 
939                 @Override
940                 public int indexOfChild(final ImmutableNode parent, final ImmutableNode child) {
941                     final List<ImmutableNode> children = super.getChildren(parent);
942                     return filterChildrenOfGlobalSection(parent, children).indexOf(child);
943                 }
944 
945                 @Override
946                 protected NodeHandler<ImmutableNode> getDecoratedNodeHandler() {
947                     return GlobalSectionNodeModel.super.getNodeHandler();
948                 }
949 
950                 /**
951                  * Filters the child nodes of the global section. This method checks whether the passed in node is the root node of the
952                  * configuration. If so, from the list of children all nodes are filtered which are section nodes.
953                  *
954                  * @param node the node in question
955                  * @param children the children of this node
956                  * @return a list with the filtered children
957                  */
958                 private List<ImmutableNode> filterChildrenOfGlobalSection(final ImmutableNode node, final List<ImmutableNode> children) {
959                     final List<ImmutableNode> filteredList;
960                     if (node == getRootNode()) {
961                         filteredList = children.stream().filter(child -> !isSectionNode(child)).collect(Collectors.toList());
962                     } else {
963                         filteredList = children;
964                     }
965 
966                     return filteredList;
967                 }
968             };
969         }
970     }
971 }