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="http://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 * @author <a
221 *         href="http://commons.apache.org/configuration/team-list.html">Commons
222 *         Configuration team</a>
223 * @since 1.6
224 */
225public class INIConfiguration extends BaseHierarchicalConfiguration implements
226        FileBasedConfiguration
227{
228    /**
229     * The default characters that signal the start of a comment line.
230     */
231    protected static final String COMMENT_CHARS = "#;";
232
233    /**
234     * The default characters used to separate keys from values.
235     */
236    protected static final String SEPARATOR_CHARS = "=:";
237
238    /**
239     * Constant for the line separator.
240     */
241    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
242
243    /**
244     * The characters used for quoting values.
245     */
246    private static final String QUOTE_CHARACTERS = "\"'";
247
248    /**
249     * The line continuation character.
250     */
251    private static final String LINE_CONT = "\\";
252
253    /**
254     * The separator used when writing an INI file.
255     */
256    private String separatorUsedInOutput = " = ";
257
258    /**
259     * The separator used when reading an INI file.
260     */
261    private String separatorUsedInInput = SEPARATOR_CHARS;
262
263    /**
264     * The characters used to separate keys from values
265     * when reading an INI file.
266     */
267    private String commentCharsUsedInInput = COMMENT_CHARS;
268
269    /**
270     * Create a new empty INI Configuration.
271     */
272    public INIConfiguration()
273    {
274        super();
275    }
276
277    /**
278     * Creates a new instance of {@code INIConfiguration} with the
279     * content of the specified {@code HierarchicalConfiguration}.
280     *
281     * @param c the configuration to be copied
282     * @since 2.0
283     */
284    public INIConfiguration(final HierarchicalConfiguration<ImmutableNode> c)
285    {
286        super(c);
287    }
288
289    /**
290     * Get separator used in INI output. see {@code setSeparatorUsedInOutput}
291     * for further explanation
292     *
293     * @return the current separator for writing the INI output
294     * @since 2.2
295     */
296    public String getSeparatorUsedInOutput()
297    {
298        beginRead(false);
299        try
300        {
301            return separatorUsedInOutput;
302        }
303        finally
304        {
305            endRead();
306        }
307    }
308
309    /**
310     * Allows setting the key and value separator which is used for the creation
311     * of the resulting INI output
312     *
313     * @param separator String of the new separator for INI output
314     * @since 2.2
315     */
316    public void setSeparatorUsedInOutput(final String separator)
317    {
318        beginWrite(false);
319        try
320        {
321            this.separatorUsedInOutput = separator;
322        }
323        finally
324        {
325            endWrite();
326        }
327    }
328
329    /**
330     * Get separator used in INI reading. see {@code setSeparatorUsedInInput}
331     * for further explanation
332     *
333     * @return the current separator for reading the INI input
334     * @since 2.5
335     */
336    public String getSeparatorUsedInInput()
337    {
338        beginRead(false);
339        try
340        {
341            return separatorUsedInInput;
342        }
343        finally
344        {
345            endRead();
346        }
347    }
348
349    /**
350     * Allows setting the key and value separator which is used in reading
351     * 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    {
358        beginRead(false);
359        try
360        {
361            this.separatorUsedInInput = separator;
362        }
363        finally
364        {
365            endRead();
366        }
367    }
368
369    /**
370     * Get comment leading separator used in INI reading.
371     * see {@code setCommentLeadingCharsUsedInInput} for further explanation
372     *
373     * @return the current separator for reading the INI input
374     * @since 2.5
375     */
376    public String getCommentLeadingCharsUsedInInput()
377    {
378        beginRead(false);
379        try
380        {
381            return commentCharsUsedInInput;
382        }
383        finally
384        {
385            endRead();
386        }
387    }
388
389    /**
390     * Allows setting the leading comment separator which is used in reading
391     * an INI file
392     *
393     * @param separator String of the new separator for INI reading
394     * @since 2.5
395     */
396    public void setCommentLeadingCharsUsedInInput(final String separator)
397    {
398        beginRead(false);
399        try
400        {
401            this.commentCharsUsedInInput = separator;
402        }
403        finally
404        {
405            endRead();
406        }
407    }
408
409    /**
410     * Save the configuration to the specified writer.
411     *
412     * @param writer - The writer to save the configuration to.
413     * @throws ConfigurationException If an error occurs while writing the
414     *         configuration
415     * @throws IOException if an I/O error occurs
416     */
417    @Override
418    public void write(final Writer writer) throws ConfigurationException, IOException
419    {
420        final PrintWriter out = new PrintWriter(writer);
421        boolean first = true;
422        final String separator = getSeparatorUsedInOutput();
423
424        beginRead(false);
425        try
426        {
427            for (final ImmutableNode node : getModel().getNodeHandler().getRootNode()
428                    .getChildren())
429            {
430                if (isSectionNode(node))
431                {
432                    if (!first)
433                    {
434                        out.println();
435                    }
436                    out.print("[");
437                    out.print(node.getNodeName());
438                    out.print("]");
439                    out.println();
440
441                    for (final ImmutableNode child : node.getChildren())
442                    {
443                        writeProperty(out, child.getNodeName(),
444                                child.getValue(), separator);
445                    }
446                }
447                else
448                {
449                    writeProperty(out, node.getNodeName(), node.getValue(), separator);
450                }
451                first = false;
452            }
453            out.println();
454            out.flush();
455        }
456        finally
457        {
458            endRead();
459        }
460    }
461
462    /**
463     * Load the configuration from the given reader. Note that the
464     * {@code clear()} method is not called so the configuration read in will
465     * be merged with the current configuration.
466     *
467     * @param in the reader to read the configuration from.
468     * @throws ConfigurationException If an error occurs while reading the
469     *         configuration
470     * @throws IOException if an I/O error occurs
471     */
472    @Override
473    public void read(final Reader in) throws ConfigurationException, IOException
474    {
475        final BufferedReader bufferedReader = new BufferedReader(in);
476        final Map<String, ImmutableNode.Builder> sectionBuilders = new LinkedHashMap<>();
477        final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
478
479        createNodeBuilders(bufferedReader, rootBuilder, sectionBuilders);
480        final ImmutableNode rootNode = createNewRootNode(rootBuilder, sectionBuilders);
481        addNodes(null, rootNode.getChildren());
482    }
483
484    /**
485     * Creates a new root node from the builders constructed while reading the
486     * configuration file.
487     *
488     * @param rootBuilder the builder for the top-level section
489     * @param sectionBuilders a map storing the section builders
490     * @return the root node of the newly created hierarchy
491     */
492    private static ImmutableNode createNewRootNode(
493            final ImmutableNode.Builder rootBuilder,
494            final Map<String, ImmutableNode.Builder> sectionBuilders)
495    {
496        for (final Map.Entry<String, ImmutableNode.Builder> e : sectionBuilders
497                .entrySet())
498        {
499            rootBuilder.addChild(e.getValue().name(e.getKey()).create());
500        }
501        return rootBuilder.create();
502    }
503
504    /**
505     * Reads the content of an INI file from the passed in reader and creates a
506     * structure of builders for constructing the {@code ImmutableNode} objects
507     * representing the data.
508     *
509     * @param in the reader
510     * @param rootBuilder the builder for the top-level section
511     * @param sectionBuilders a map storing the section builders
512     * @throws IOException if an I/O error occurs
513     */
514    private void createNodeBuilders(final BufferedReader in,
515            final ImmutableNode.Builder rootBuilder,
516            final Map<String, ImmutableNode.Builder> sectionBuilders)
517            throws IOException
518    {
519        ImmutableNode.Builder sectionBuilder = rootBuilder;
520        String line = in.readLine();
521        while (line != null)
522        {
523            line = line.trim();
524            if (!isCommentLine(line))
525            {
526                if (isSectionLine(line))
527                {
528                    final String section = line.substring(1, line.length() - 1);
529                    sectionBuilder = sectionBuilders.get(section);
530                    if (sectionBuilder == null)
531                    {
532                        sectionBuilder = new ImmutableNode.Builder();
533                        sectionBuilders.put(section, sectionBuilder);
534                    }
535                }
536
537                else
538                {
539                    String key;
540                    String value = "";
541                    final int index = findSeparator(line);
542                    if (index >= 0)
543                    {
544                        key = line.substring(0, index);
545                        value = parseValue(line.substring(index + 1), in);
546                    }
547                    else
548                    {
549                        key = line;
550                    }
551                    key = key.trim();
552                    if (key.length() < 1)
553                    {
554                        // use space for properties with no key
555                        key = " ";
556                    }
557                    createValueNodes(sectionBuilder, key, value);
558                }
559            }
560
561            line = in.readLine();
562        }
563    }
564
565    /**
566     * Creates the node(s) for the given key value-pair. If delimiter parsing is
567     * enabled, the value string is split if possible, and for each single value
568     * a node is created. Otherwise only a single node is added to the section.
569     *
570     * @param sectionBuilder the section builder for adding new nodes
571     * @param key the key
572     * @param value the value string
573     */
574    private void createValueNodes(final ImmutableNode.Builder sectionBuilder,
575            final String key, final String value)
576    {
577        final Collection<String> values =
578                getListDelimiterHandler().split(value, false);
579
580        for (final String v : values)
581        {
582            sectionBuilder.addChild(new ImmutableNode.Builder().name(key)
583                    .value(v).create());
584        }
585    }
586
587    /**
588     * Writes data about a property into the given stream.
589     *
590     * @param out the output stream
591     * @param key the key
592     * @param value the value
593     */
594    private void writeProperty(final PrintWriter out, final String key, final Object value, final String separator)
595    {
596        out.print(key);
597        out.print(separator);
598        out.print(escapeValue(value.toString()));
599        out.println();
600    }
601
602    /**
603     * Parse the value to remove the quotes and ignoring the comment. Example:
604     *
605     * <pre>
606     * &quot;value&quot; ; comment -&gt; value
607     * </pre>
608     *
609     * <pre>
610     * 'value' ; comment -&gt; value
611     * </pre>
612     * Note that a comment character is only recognized if there is at least one
613     * whitespace character before it. So it can appear in the property value,
614     * e.g.:
615     * <pre>
616     * C:\\Windows;C:\\Windows\\system32
617     * </pre>
618     *
619     * @param val the value to be parsed
620     * @param reader the reader (needed if multiple lines have to be read)
621     * @throws IOException if an IO error occurs
622     */
623    private String parseValue(final String val, final BufferedReader reader)
624        throws IOException
625    {
626        final StringBuilder propertyValue = new StringBuilder();
627        boolean lineContinues;
628        String value = val.trim();
629
630        do
631        {
632            final boolean quoted = value.startsWith("\"") || value.startsWith("'");
633            boolean stop = false;
634            boolean escape = false;
635
636            final char quote = quoted ? value.charAt(0) : 0;
637
638            int i = quoted ? 1 : 0;
639
640            final StringBuilder result = new StringBuilder();
641            char lastChar = 0;
642            while (i < value.length() && !stop)
643            {
644                final char c = value.charAt(i);
645
646                if (quoted)
647                {
648                    if ('\\' == c && !escape)
649                    {
650                        escape = true;
651                    }
652                    else if (!escape && quote == c)
653                    {
654                        stop = true;
655                    }
656                    else if (escape && quote == c)
657                    {
658                        escape = false;
659                        result.append(c);
660                    }
661                    else
662                    {
663                        if (escape)
664                        {
665                            escape = false;
666                            result.append('\\');
667                        }
668
669                        result.append(c);
670                    }
671                }
672                else
673                {
674                    if (isCommentChar(c) && Character.isWhitespace(lastChar))
675                    {
676                        stop = true;
677                    }
678                    else
679                    {
680                        result.append(c);
681                    }
682                }
683
684                i++;
685                lastChar = c;
686            }
687
688            String v = result.toString();
689            if (!quoted)
690            {
691                v = v.trim();
692                lineContinues = lineContinues(v);
693                if (lineContinues)
694                {
695                    // remove trailing "\"
696                    v = v.substring(0, v.length() - 1).trim();
697                }
698            }
699            else
700            {
701                lineContinues = lineContinues(value, i);
702            }
703            propertyValue.append(v);
704
705            if (lineContinues)
706            {
707                propertyValue.append(LINE_SEPARATOR);
708                value = reader.readLine();
709            }
710        } while (lineContinues && value != null);
711
712        return propertyValue.toString();
713    }
714
715    /**
716     * Tests whether the specified string contains a line continuation marker.
717     *
718     * @param line the string to check
719     * @return a flag whether this line continues
720     */
721    private static boolean lineContinues(final String line)
722    {
723        final String s = line.trim();
724        return s.equals(LINE_CONT)
725                || (s.length() > 2 && s.endsWith(LINE_CONT) && Character
726                        .isWhitespace(s.charAt(s.length() - 2)));
727    }
728
729    /**
730     * Tests whether the specified string contains a line continuation marker
731     * after the specified position. This method parses the string to remove a
732     * comment that might be present. Then it checks whether a line continuation
733     * marker can be found at the end.
734     *
735     * @param line the line to check
736     * @param pos the start position
737     * @return a flag whether this line continues
738     */
739    private boolean lineContinues(final String line, final int pos)
740    {
741        String s;
742
743        if (pos >= line.length())
744        {
745            s = line;
746        }
747        else
748        {
749            int end = pos;
750            while (end < line.length() && !isCommentChar(line.charAt(end)))
751            {
752                end++;
753            }
754            s = line.substring(pos, end);
755        }
756
757        return lineContinues(s);
758    }
759
760    /**
761     * Tests whether the specified character is a comment character.
762     *
763     * @param c the character
764     * @return a flag whether this character starts a comment
765     */
766    private boolean isCommentChar(final char c)
767    {
768        return getCommentLeadingCharsUsedInInput().indexOf(c) >= 0;
769    }
770
771    /**
772     * Tries to find the index of the separator character in the given string.
773     * This method checks for the presence of separator characters in the given
774     * string. If multiple characters are found, the first one is assumed to be
775     * the correct separator. If there are quoting characters, they are taken
776     * into account, too.
777     *
778     * @param line the line to be checked
779     * @return the index of the separator character or -1 if none is found
780     */
781    private int findSeparator(final String line)
782    {
783        int index =
784                findSeparatorBeforeQuote(line,
785                        findFirstOccurrence(line, QUOTE_CHARACTERS));
786        if (index < 0)
787        {
788            index = findFirstOccurrence(line, getSeparatorUsedInInput());
789        }
790        return index;
791    }
792
793    /**
794     * Checks for the occurrence of the specified separators in the given line.
795     * The index of the first separator is returned.
796     *
797     * @param line the line to be investigated
798     * @param separators a string with the separator characters to look for
799     * @return the lowest index of a separator character or -1 if no separator
800     *         is found
801     */
802    private static int findFirstOccurrence(final String line, final String separators)
803    {
804        int index = -1;
805
806        for (int i = 0; i < separators.length(); i++)
807        {
808            final char sep = separators.charAt(i);
809            final int pos = line.indexOf(sep);
810            if (pos >= 0)
811            {
812                if (index < 0 || pos < index)
813                {
814                    index = pos;
815                }
816            }
817        }
818
819        return index;
820    }
821
822    /**
823     * Searches for a separator character directly before a quoting character.
824     * If the first non-whitespace character before a quote character is a
825     * separator, it is considered the "real" separator in this line - even if
826     * there are other separators before.
827     *
828     * @param line the line to be investigated
829     * @param quoteIndex the index of the quote character
830     * @return the index of the separator before the quote or &lt; 0 if there is
831     *         none
832     */
833    private static int findSeparatorBeforeQuote(final String line, final int quoteIndex)
834    {
835        int index = quoteIndex - 1;
836        while (index >= 0 && Character.isWhitespace(line.charAt(index)))
837        {
838            index--;
839        }
840
841        if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0)
842        {
843            index = -1;
844        }
845
846        return index;
847    }
848
849    /**
850     * Escapes the given property value before it is written. This method add
851     * quotes around the specified value if it contains a comment character and
852     * handles list delimiter characters.
853     *
854     * @param value the string to be escaped
855     */
856    private String escapeValue(final String value)
857    {
858        return String.valueOf(getListDelimiterHandler().escape(
859                escapeComments(value), ListDelimiterHandler.NOOP_TRANSFORMER));
860    }
861
862    /**
863     * Escapes comment characters in the given value.
864     *
865     * @param value the value to be escaped
866     * @return the value with comment characters escaped
867     */
868    private String escapeComments(final String value)
869    {
870        final String commentChars = getCommentLeadingCharsUsedInInput();
871        boolean quoted = false;
872
873        for (int i = 0; i < commentChars.length() && !quoted; i++)
874        {
875            final char c = commentChars.charAt(i);
876            if (value.indexOf(c) != -1)
877            {
878                quoted = true;
879            }
880        }
881
882        if (quoted)
883        {
884            return '"' + value.replaceAll("\"", "\\\\\\\"") + '"';
885        }
886        return value;
887    }
888
889    /**
890     * Determine if the given line is a comment line.
891     *
892     * @param line The line to check.
893     * @return true if the line is empty or starts with one of the comment
894     *         characters
895     */
896    protected boolean isCommentLine(final String line)
897    {
898        if (line == null)
899        {
900            return false;
901        }
902        // blank lines are also treated as comment lines
903        return line.length() < 1
904            || getCommentLeadingCharsUsedInInput().indexOf(line.charAt(0)) >= 0;
905    }
906
907    /**
908     * Determine if the given line is a section.
909     *
910     * @param line The line to check.
911     * @return true if the line contains a section
912     */
913    protected boolean isSectionLine(final String line)
914    {
915        if (line == null)
916        {
917            return false;
918        }
919        return line.startsWith("[") && line.endsWith("]");
920    }
921
922    /**
923     * Return a set containing the sections in this ini configuration. Note that
924     * changes to this set do not affect the configuration.
925     *
926     * @return a set containing the sections.
927     */
928    public Set<String> getSections()
929    {
930        final Set<String> sections = new LinkedHashSet<>();
931        boolean globalSection = false;
932        boolean inSection = false;
933
934        beginRead(false);
935        try
936        {
937            for (final ImmutableNode node : getModel().getNodeHandler().getRootNode()
938                    .getChildren())
939            {
940                if (isSectionNode(node))
941                {
942                    inSection = true;
943                    sections.add(node.getNodeName());
944                }
945                else
946                {
947                    if (!inSection && !globalSection)
948                    {
949                        globalSection = true;
950                        sections.add(null);
951                    }
952                }
953            }
954        }
955        finally
956        {
957            endRead();
958        }
959
960        return sections;
961    }
962
963    /**
964     * Returns a configuration with the content of the specified section. This
965     * provides an easy way of working with a single section only. The way this
966     * configuration is structured internally, this method is very similar to
967     * calling {@link HierarchicalConfiguration#configurationAt(String)} with
968     * the name of the section in question. There are the following differences
969     * however:
970     * <ul>
971     * <li>This method never throws an exception. If the section does not exist,
972     * it is created now. The configuration returned in this case is empty.</li>
973     * <li>If section is contained multiple times in the configuration, the
974     * configuration returned by this method is initialized with the first
975     * occurrence of the section. (This can only happen if
976     * {@code addProperty()} has been used in a way that does not conform
977     * to the storage scheme used by {@code INIConfiguration}.
978     * If used correctly, there will not be duplicate sections.)</li>
979     * <li>There is special support for the global section: Passing in
980     * <b>null</b> as section name returns a configuration with the content of
981     * the global section (which may also be empty).</li>
982     * </ul>
983     *
984     * @param name the name of the section in question; <b>null</b> represents
985     *        the global section
986     * @return a configuration containing only the properties of the specified
987     *         section
988     */
989    public SubnodeConfiguration getSection(final String name)
990    {
991        if (name == null)
992        {
993            return getGlobalSection();
994        }
995        try
996        {
997            return (SubnodeConfiguration) configurationAt(name, true);
998        }
999        catch (final ConfigurationRuntimeException iex)
1000        {
1001            // the passed in key does not map to exactly one node
1002            // obtain the node for the section, create it on demand
1003            final InMemoryNodeModel parentModel = getSubConfigurationParentModel();
1004            final NodeSelector selector = parentModel.trackChildNodeWithCreation(null, name, this);
1005            return createSubConfigurationForTrackedNode(selector, this);
1006        }
1007    }
1008
1009    /**
1010     * Creates a sub configuration for the global section of the represented INI
1011     * configuration.
1012     *
1013     * @return the sub configuration for the global section
1014     */
1015    private SubnodeConfiguration getGlobalSection()
1016    {
1017        final InMemoryNodeModel parentModel = getSubConfigurationParentModel();
1018        final NodeSelector selector = new NodeSelector(null); // selects parent
1019        parentModel.trackNode(selector, this);
1020        final GlobalSectionNodeModel model =
1021                new GlobalSectionNodeModel(this, selector);
1022        final SubnodeConfiguration sub = new SubnodeConfiguration(this, model);
1023        initSubConfigurationForThisParent(sub);
1024        return sub;
1025    }
1026
1027    /**
1028     * Checks whether the specified configuration node represents a section.
1029     *
1030     * @param node the node in question
1031     * @return a flag whether this node represents a section
1032     */
1033    private static boolean isSectionNode(final ImmutableNode node)
1034    {
1035        return node.getValue() == null;
1036    }
1037
1038    /**
1039     * A specialized node model implementation for the sub configuration
1040     * representing the global section of the INI file. This is a regular
1041     * {@code TrackedNodeModel} with one exception: The {@code NodeHandler} used
1042     * by this model applies a filter on the children of the root node so that
1043     * only nodes are visible that are no sub sections.
1044     */
1045    private static class GlobalSectionNodeModel extends TrackedNodeModel
1046    {
1047        /**
1048         * Creates a new instance of {@code GlobalSectionNodeModel} and
1049         * initializes it with the given underlying model.
1050         *
1051         * @param modelSupport the underlying {@code InMemoryNodeModel}
1052         * @param selector the {@code NodeSelector}
1053         */
1054        public GlobalSectionNodeModel(final InMemoryNodeModelSupport modelSupport,
1055                final NodeSelector selector)
1056        {
1057            super(modelSupport, selector, true);
1058        }
1059
1060        @Override
1061        public NodeHandler<ImmutableNode> getNodeHandler()
1062        {
1063            return new NodeHandlerDecorator<ImmutableNode>()
1064            {
1065                @Override
1066                public List<ImmutableNode> getChildren(final ImmutableNode node)
1067                {
1068                    final List<ImmutableNode> children = super.getChildren(node);
1069                    return filterChildrenOfGlobalSection(node, children);
1070                }
1071
1072                @Override
1073                public List<ImmutableNode> getChildren(final ImmutableNode node,
1074                        final String name)
1075                {
1076                    final List<ImmutableNode> children =
1077                            super.getChildren(node, name);
1078                    return filterChildrenOfGlobalSection(node, children);
1079                }
1080
1081                @Override
1082                public int getChildrenCount(final ImmutableNode node, final String name)
1083                {
1084                    final List<ImmutableNode> children =
1085                            (name != null) ? super.getChildren(node, name)
1086                                    : super.getChildren(node);
1087                    return filterChildrenOfGlobalSection(node, children).size();
1088                }
1089
1090                @Override
1091                public ImmutableNode getChild(final ImmutableNode node, final int index)
1092                {
1093                    final List<ImmutableNode> children = super.getChildren(node);
1094                    return filterChildrenOfGlobalSection(node, children).get(
1095                            index);
1096                }
1097
1098                @Override
1099                public int indexOfChild(final ImmutableNode parent,
1100                        final ImmutableNode child)
1101                {
1102                    final List<ImmutableNode> children = super.getChildren(parent);
1103                    return filterChildrenOfGlobalSection(parent, children)
1104                            .indexOf(child);
1105                }
1106
1107                @Override
1108                protected NodeHandler<ImmutableNode> getDecoratedNodeHandler()
1109                {
1110                    return GlobalSectionNodeModel.super.getNodeHandler();
1111                }
1112
1113                /**
1114                 * Filters the child nodes of the global section. This method
1115                 * checks whether the passed in node is the root node of the
1116                 * configuration. If so, from the list of children all nodes are
1117                 * filtered which are section nodes.
1118                 *
1119                 * @param node the node in question
1120                 * @param children the children of this node
1121                 * @return a list with the filtered children
1122                 */
1123                private List<ImmutableNode> filterChildrenOfGlobalSection(
1124                        final ImmutableNode node, final List<ImmutableNode> children)
1125                {
1126                    List<ImmutableNode> filteredList;
1127                    if (node == getRootNode())
1128                    {
1129                        filteredList =
1130                                new ArrayList<>(children.size());
1131                        for (final ImmutableNode child : children)
1132                        {
1133                            if (!isSectionNode(child))
1134                            {
1135                                filteredList.add(child);
1136                            }
1137                        }
1138                    }
1139                    else
1140                    {
1141                        filteredList = children;
1142                    }
1143
1144                    return filteredList;
1145                }
1146            };
1147        }
1148    }
1149}