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