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