INIConfiguration.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.configuration2;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.PrintWriter;
  21. import java.io.Reader;
  22. import java.io.Writer;
  23. import java.util.LinkedHashMap;
  24. import java.util.LinkedHashSet;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Set;
  28. import java.util.stream.Collectors;

  29. import org.apache.commons.configuration2.convert.ListDelimiterHandler;
  30. import org.apache.commons.configuration2.ex.ConfigurationException;
  31. import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
  32. import org.apache.commons.configuration2.tree.ImmutableNode;
  33. import org.apache.commons.configuration2.tree.InMemoryNodeModel;
  34. import org.apache.commons.configuration2.tree.InMemoryNodeModelSupport;
  35. import org.apache.commons.configuration2.tree.NodeHandler;
  36. import org.apache.commons.configuration2.tree.NodeHandlerDecorator;
  37. import org.apache.commons.configuration2.tree.NodeSelector;
  38. import org.apache.commons.configuration2.tree.TrackedNodeModel;

  39. /**
  40.  * <p>
  41.  * A specialized hierarchical configuration implementation for parsing ini files.
  42.  * </p>
  43.  * <p>
  44.  * An initialization or ini file is a configuration file typically found on Microsoft's Windows operating system and
  45.  * contains data for Windows based applications.
  46.  * </p>
  47.  * <p>
  48.  * Although popularized by Windows, ini files can be used on any system or platform due to the fact that they are merely
  49.  * text files that can easily be parsed and modified by both humans and computers.
  50.  * </p>
  51.  * <p>
  52.  * A typical ini file could look something like:
  53.  * </p>
  54.  *
  55.  * <pre>
  56.  * [section1]
  57.  * ; this is a comment!
  58.  * var1 = foo
  59.  * var2 = bar
  60.  *
  61.  * [section2]
  62.  * var1 = doo
  63.  * </pre>
  64.  * <p>
  65.  * The format of ini files is fairly straight forward and is composed of three components:
  66.  * </p>
  67.  * <ul>
  68.  * <li><strong>Sections:</strong> Ini files are split into sections, each section starting with a section declaration. A section
  69.  * declaration starts with a '[' and ends with a ']'. Sections occur on one line only.</li>
  70.  * <li><strong>Parameters:</strong> Items in a section are known as parameters. Parameters have a typical {@code key = value}
  71.  * format.</li>
  72.  * <li><strong>Comments:</strong> Lines starting with a ';' are assumed to be comments.</li>
  73.  * </ul>
  74.  * <p>
  75.  * There are various implementations of the ini file format by various vendors which has caused a number of differences
  76.  * to appear. As far as possible this configuration tries to be lenient and support most of the differences.
  77.  * </p>
  78.  * <p>
  79.  * Some of the differences supported are as follows:
  80.  * </p>
  81.  * <ul>
  82.  * <li><strong>Comments:</strong> The '#' character is also accepted as a comment signifier.</li>
  83.  * <li><strong>Key value separator:</strong> The ':' character is also accepted in place of '=' to separate keys and values in
  84.  * parameters, for example {@code var1 : foo}.</li>
  85.  * <li><strong>Duplicate sections:</strong> Typically duplicate sections are not allowed, this configuration does however support
  86.  * this feature. In the event of a duplicate section, the two section's values are merged so that there is only a single
  87.  * section. <strong>Note</strong>: This also affects the internal data of the configuration. If it is saved, only a
  88.  * single section is written!</li>
  89.  * <li><strong>Duplicate parameters:</strong> Typically duplicate parameters are only allowed if they are in two different
  90.  * sections, thus they are local to sections; this configuration simply merges duplicates; if a section has a duplicate
  91.  * parameter the values are then added to the key as a list.</li>
  92.  * </ul>
  93.  * <p>
  94.  * Global parameters are also allowed; any parameters declared before a section is declared are added to a global
  95.  * section. It is important to note that this global section does not have a name.
  96.  * </p>
  97.  * <p>
  98.  * In all instances, a parameter's key is prepended with its section name and a '.' (period). Thus a parameter named
  99.  * "var1" in "section1" will have the key {@code section1.var1} in this configuration. (This is the default behavior.
  100.  * Because this is a hierarchical configuration you can change this by setting a different
  101.  * {@link org.apache.commons.configuration2.tree.ExpressionEngine}.)
  102.  * </p>
  103.  * <p>
  104.  * <strong>Implementation Details:</strong> Consider the following ini file:
  105.  * </p>
  106.  * <pre>
  107.  *  default = ok
  108.  *
  109.  *  [section1]
  110.  *  var1 = foo
  111.  *  var2 = doodle
  112.  *
  113.  *  [section2]
  114.  *  ; a comment
  115.  *  var1 = baz
  116.  *  var2 = shoodle
  117.  *  bad =
  118.  *  = worse
  119.  *
  120.  *  [section3]
  121.  *  # another comment
  122.  *  var1 : foo
  123.  *  var2 : bar
  124.  *  var5 : test1
  125.  *
  126.  *  [section3]
  127.  *  var3 = foo
  128.  *  var4 = bar
  129.  *  var5 = test2
  130.  *
  131.  *  [sectionSeparators]
  132.  *  passwd : abc=def
  133.  *  a:b = "value"
  134.  *
  135.  *  []
  136.  *  var = emptySection
  137.  * </pre>
  138.  * <p>
  139.  * This ini file will be parsed without error. Note:
  140.  * </p>
  141.  * <ul>
  142.  * <li>The parameter named "default" is added to the global section, it's value is accessed simply using
  143.  * {@code getProperty("default")}.</li>
  144.  * <li>Section 1's parameters can be accessed using {@code getProperty("section1.var1")}.</li>
  145.  * <li>The parameter named "bad" simply adds the parameter with an empty value.</li>
  146.  * <li>The empty key with value "= worse" is added using a key consisting of a single space character. This key is still
  147.  * added to section 2 and the value can be accessed using {@code getProperty("section2. ")}, notice the period '.' and
  148.  * the space following the section name.</li>
  149.  * <li>Section three uses both '=' and ':' to separate keys and values.</li>
  150.  * <li>Section 3 has a duplicate key named "var5". The value for this key is [test1, test2], and is represented as a
  151.  * List.</li>
  152.  * <li>The section called <em>sectionSeparators</em> demonstrates how the configuration deals with multiple occurrences
  153.  * of separator characters. Per default the first separator character in a line is detected and used to split the key
  154.  * from the value. Therefore the first property definition in this section has the key {@code passwd} and the value
  155.  * {@code abc=def}. This default behavior can be changed by using quotes. If there is a separator character before the
  156.  * first quote character (ignoring whitespace), this character is used as separator. Thus the second property definition
  157.  * in the section has the key {@code a:b} and the value {@code value}.</li>
  158.  * <li>The empty section is added using a key consisting of a single space character. The parameters can be accessed
  159.  * using {@code getProperty(" .var")}</li>
  160.  * </ul>
  161.  * <p>
  162.  * Internally, this configuration maps the content of the represented ini file to its node structure in the following
  163.  * way:
  164.  * </p>
  165.  * <ul>
  166.  * <li>Sections are represented by direct child nodes of the root node.</li>
  167.  * <li>For the content of a section, corresponding nodes are created as children of the section node.</li>
  168.  * </ul>
  169.  * <p>
  170.  * This explains how the keys for the properties can be constructed. You can also use other methods of
  171.  * {@link HierarchicalConfiguration} for querying or manipulating the hierarchy of configuration nodes, for instance the
  172.  * {@code configurationAt()} method for obtaining the data of a specific section. However, be careful that the storage
  173.  * scheme described above is not violated (for example by adding multiple levels of nodes or inserting duplicate section
  174.  * nodes). Otherwise, the special methods for ini configurations may not work correctly!
  175.  * </p>
  176.  * <p>
  177.  * The set of sections in this configuration can be retrieved using the {@code getSections()} method. For obtaining a
  178.  * {@code SubnodeConfiguration} with the content of a specific section the {@code getSection()} method can be used.
  179.  * </p>
  180.  * <p>
  181.  * Like other {@code Configuration} implementations, this class uses a {@code Synchronizer} object to control concurrent
  182.  * access. By choosing a suitable implementation of the {@code Synchronizer} interface, an instance can be made
  183.  * thread-safe or not. Note that access to most of the properties typically set through a builder is not protected by
  184.  * the {@code Synchronizer}. The intended usage is that these properties are set once at construction time through the
  185.  * builder and after that remain constant. If you wish to change such properties during life time of an instance, you
  186.  * have to use the {@code lock()} and {@code unlock()} methods manually to ensure that other threads see your changes.
  187.  * </p>
  188.  * <p>
  189.  * As this class extends {@link AbstractConfiguration}, all basic features like variable interpolation, list handling,
  190.  * or data type conversions are available as well. This is described in the chapter
  191.  * <a href="https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html"> Basic features
  192.  * and AbstractConfiguration</a> of the user's guide.
  193.  * </p>
  194.  * <p>
  195.  * Note that this configuration does not support properties with null values. Such properties are considered to be
  196.  * section nodes.
  197.  * </p>
  198.  *
  199.  * @since 1.6
  200.  */
  201. public class INIConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration {

  202.     /**
  203.      * Builds instances of INIConfiguration.
  204.      *
  205.      * @since 2.9.0
  206.      */
  207.     public static class Builder {

  208.         /**
  209.          * Whether in-line comments on the section line are allowed.
  210.          */
  211.         private boolean sectionInLineCommentsAllowed;

  212.         /**
  213.          * Builds a new INIConfiguration.
  214.          *
  215.          * @return a new INIConfiguration.
  216.          */
  217.         public INIConfiguration build() {
  218.             return new INIConfiguration(sectionInLineCommentsAllowed);
  219.         }

  220.         /**
  221.          * Sets whether in-line comments on the section line are allowed.
  222.          *
  223.          * @param sectionInLineCommentsAllowed Whether in-line comments on the section line are allowed.
  224.          * @return this instance.
  225.          */
  226.         public Builder setSectionInLineCommentsAllowed(final boolean sectionInLineCommentsAllowed) {
  227.             this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed;
  228.             return this;
  229.         }

  230.     }

  231.     /**
  232.      * A specialized node model implementation for the sub configuration representing the global section of the INI file.
  233.      * This is a regular {@code TrackedNodeModel} with one exception: The {@code NodeHandler} used by this model applies a
  234.      * filter on the children of the root node so that only nodes are visible that are no sub sections.
  235.      */
  236.     private static final class GlobalSectionNodeModel extends TrackedNodeModel {
  237.         /**
  238.          * Creates a new instance of {@code GlobalSectionNodeModel} and initializes it with the given underlying model.
  239.          *
  240.          * @param modelSupport the underlying {@code InMemoryNodeModel}
  241.          * @param selector the {@code NodeSelector}
  242.          */
  243.         public GlobalSectionNodeModel(final InMemoryNodeModelSupport modelSupport, final NodeSelector selector) {
  244.             super(modelSupport, selector, true);
  245.         }

  246.         @Override
  247.         public NodeHandler<ImmutableNode> getNodeHandler() {
  248.             return new NodeHandlerDecorator<ImmutableNode>() {
  249.                 /**
  250.                  * Filters the child nodes of the global section. This method checks whether the passed in node is the root node of the
  251.                  * configuration. If so, from the list of children all nodes are filtered which are section nodes.
  252.                  *
  253.                  * @param node the node in question
  254.                  * @param children the children of this node
  255.                  * @return a list with the filtered children
  256.                  */
  257.                 private List<ImmutableNode> filterChildrenOfGlobalSection(final ImmutableNode node, final List<ImmutableNode> children) {
  258.                     final List<ImmutableNode> filteredList;
  259.                     if (node == getRootNode()) {
  260.                         filteredList = children.stream().filter(child -> !isSectionNode(child)).collect(Collectors.toList());
  261.                     } else {
  262.                         filteredList = children;
  263.                     }

  264.                     return filteredList;
  265.                 }

  266.                 @Override
  267.                 public ImmutableNode getChild(final ImmutableNode node, final int index) {
  268.                     final List<ImmutableNode> children = super.getChildren(node);
  269.                     return filterChildrenOfGlobalSection(node, children).get(index);
  270.                 }

  271.                 @Override
  272.                 public List<ImmutableNode> getChildren(final ImmutableNode node) {
  273.                     final List<ImmutableNode> children = super.getChildren(node);
  274.                     return filterChildrenOfGlobalSection(node, children);
  275.                 }

  276.                 @Override
  277.                 public List<ImmutableNode> getChildren(final ImmutableNode node, final String name) {
  278.                     final List<ImmutableNode> children = super.getChildren(node, name);
  279.                     return filterChildrenOfGlobalSection(node, children);
  280.                 }

  281.                 @Override
  282.                 public int getChildrenCount(final ImmutableNode node, final String name) {
  283.                     final List<ImmutableNode> children = name != null ? super.getChildren(node, name) : super.getChildren(node);
  284.                     return filterChildrenOfGlobalSection(node, children).size();
  285.                 }

  286.                 @Override
  287.                 protected NodeHandler<ImmutableNode> getDecoratedNodeHandler() {
  288.                     return GlobalSectionNodeModel.super.getNodeHandler();
  289.                 }

  290.                 @Override
  291.                 public int indexOfChild(final ImmutableNode parent, final ImmutableNode child) {
  292.                     final List<ImmutableNode> children = super.getChildren(parent);
  293.                     return filterChildrenOfGlobalSection(parent, children).indexOf(child);
  294.                 }
  295.             };
  296.         }
  297.     }

  298.     /**
  299.      * The empty key.
  300.      */
  301.     private static final String EMPTY_KEY = " ";

  302.     /**
  303.      * The default characters that signal the start of a comment line.
  304.      */
  305.     protected static final String COMMENT_CHARS = "#;";

  306.     /**
  307.      * The default characters used to separate keys from values.
  308.      */
  309.     protected static final String SEPARATOR_CHARS = "=:";

  310.     /**
  311.      * Constant for the line separator.
  312.      */
  313.     private static final String LINE_SEPARATOR = System.lineSeparator();

  314.     /**
  315.      * The characters used for quoting values.
  316.      */
  317.     private static final String QUOTE_CHARACTERS = "\"'";

  318.     /**
  319.      * The line continuation character.
  320.      */
  321.     private static final String LINE_CONT = "\\";

  322.     /**
  323.      * Creates a new builder.
  324.      *
  325.      * @return a new builder.
  326.      * @since 2.9.0
  327.      */
  328.     public static Builder builder() {
  329.         return new Builder();
  330.     }

  331.     /**
  332.      * Creates a new root node from the builders constructed while reading the configuration file.
  333.      *
  334.      * @param rootBuilder the builder for the top-level section
  335.      * @param sectionBuilders a map storing the section builders
  336.      * @return the root node of the newly created hierarchy
  337.      */
  338.     private static ImmutableNode createNewRootNode(final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders) {
  339.         sectionBuilders.forEach((k, v) -> rootBuilder.addChild(v.name(k).create()));
  340.         return rootBuilder.create();
  341.     }

  342.     /**
  343.      * Checks for the occurrence of the specified separators in the given line. The index of the first separator is
  344.      * returned.
  345.      *
  346.      * @param line the line to be investigated
  347.      * @param separators a string with the separator characters to look for
  348.      * @return the lowest index of a separator character or -1 if no separator is found
  349.      */
  350.     private static int findFirstOccurrence(final String line, final String separators) {
  351.         int index = -1;

  352.         for (int i = 0; i < separators.length(); i++) {
  353.             final char sep = separators.charAt(i);
  354.             final int pos = line.indexOf(sep);
  355.             if (pos >= 0 && (index < 0 || pos < index)) {
  356.                 index = pos;
  357.             }
  358.         }

  359.         return index;
  360.     }

  361.     /**
  362.      * Searches for a separator character directly before a quoting character. If the first non-whitespace character before
  363.      * a quote character is a separator, it is considered the "real" separator in this line - even if there are other
  364.      * separators before.
  365.      *
  366.      * @param line the line to be investigated
  367.      * @param quoteIndex the index of the quote character
  368.      * @return the index of the separator before the quote or &lt; 0 if there is none
  369.      */
  370.     private static int findSeparatorBeforeQuote(final String line, final int quoteIndex) {
  371.         int index = quoteIndex - 1;
  372.         while (index >= 0 && Character.isWhitespace(line.charAt(index))) {
  373.             index--;
  374.         }

  375.         if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0) {
  376.             index = -1;
  377.         }

  378.         return index;
  379.     }

  380.     /**
  381.      * Determine if the given line contains a section - inline comments are allowed.
  382.      *
  383.      * @param line The line to check.
  384.      * @return true if the line contains a section
  385.      */
  386.     private static boolean isNonStrictSection(final String line) {
  387.         return line.startsWith("[") && line.contains("]");
  388.     }

  389.     /**
  390.      * Checks whether the specified configuration node represents a section.
  391.      *
  392.      * @param node the node in question
  393.      * @return a flag whether this node represents a section
  394.      */
  395.     private static boolean isSectionNode(final ImmutableNode node) {
  396.         return node.getValue() == null;
  397.     }

  398.     /**
  399.      * Determine if the entire given line is a section - inline comments are not allowed.
  400.      *
  401.      * @param line The line to check.
  402.      * @return true if the entire line is a section
  403.      */
  404.     private static boolean isStrictSection(final String line) {
  405.         return line.startsWith("[") && line.endsWith("]");
  406.     }

  407.     /**
  408.      * Tests whether the specified string contains a line continuation marker.
  409.      *
  410.      * @param line the string to check
  411.      * @return a flag whether this line continues
  412.      */
  413.     private static boolean lineContinues(final String line) {
  414.         final String s = line.trim();
  415.         return s.equals(LINE_CONT) || s.length() > 2 && s.endsWith(LINE_CONT) && Character.isWhitespace(s.charAt(s.length() - 2));
  416.     }

  417.     /**
  418.      * The separator used when writing an INI file.
  419.      */
  420.     private String separatorUsedInOutput = " = ";

  421.     /**
  422.      * The separator used when reading an INI file.
  423.      */
  424.     private String separatorUsedInInput = SEPARATOR_CHARS;

  425.     /**
  426.      * The characters used to separate keys from values when reading an INI file.
  427.      */
  428.     private String commentCharsUsedInInput = COMMENT_CHARS;

  429.     /**
  430.      * The flag for decision, whether inline comments on the section line are allowed.
  431.      */
  432.     private boolean sectionInLineCommentsAllowed;

  433.     /**
  434.      * Create a new empty INI Configuration.
  435.      */
  436.     public INIConfiguration() {
  437.     }

  438.     /**
  439.      * Create a new empty INI Configuration with option to allow inline comments on the section line.
  440.      *
  441.      * @param sectionInLineCommentsAllowed when true inline comments on the section line are allowed
  442.      */
  443.     private INIConfiguration(final boolean sectionInLineCommentsAllowed) {
  444.         this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed;
  445.     }

  446.     /**
  447.      * Creates a new instance of {@code INIConfiguration} with the content of the specified
  448.      * {@code HierarchicalConfiguration}.
  449.      *
  450.      * @param c the configuration to be copied
  451.      * @since 2.0
  452.      */
  453.     public INIConfiguration(final HierarchicalConfiguration<ImmutableNode> c) {
  454.         super(c);
  455.     }

  456.     /**
  457.      * Reads the content of an INI file from the passed in reader and creates a structure of builders for constructing the
  458.      * {@code ImmutableNode} objects representing the data.
  459.      *
  460.      * @param in the reader
  461.      * @param rootBuilder the builder for the top-level section
  462.      * @param sectionBuilders a map storing the section builders
  463.      * @throws IOException if an I/O error occurs.
  464.      */
  465.     private void createNodeBuilders(final BufferedReader in, final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders)
  466.         throws IOException {
  467.         ImmutableNode.Builder sectionBuilder = rootBuilder;
  468.         String line = in.readLine();
  469.         while (line != null) {
  470.             line = line.trim();
  471.             if (!isCommentLine(line)) {
  472.                 if (isSectionLine(line)) {
  473.                     final int length = sectionInLineCommentsAllowed ? line.indexOf("]") : line.length() - 1;
  474.                     String section = line.substring(1, length);
  475.                     if (section.isEmpty()) {
  476.                         // use space for sections with no key
  477.                         section = EMPTY_KEY;
  478.                     }
  479.                     sectionBuilder = sectionBuilders.computeIfAbsent(section, k -> new ImmutableNode.Builder());
  480.                 } else {
  481.                     String key;
  482.                     String value = "";
  483.                     final int index = findSeparator(line);
  484.                     if (index >= 0) {
  485.                         key = line.substring(0, index);
  486.                         value = parseValue(line.substring(index + 1), in);
  487.                     } else {
  488.                         key = line;
  489.                     }
  490.                     key = key.trim();
  491.                     if (key.isEmpty()) {
  492.                         // use space for properties with no key
  493.                         key = EMPTY_KEY;
  494.                     }
  495.                     createValueNodes(sectionBuilder, key, value);
  496.                 }
  497.             }

  498.             line = in.readLine();
  499.         }
  500.     }

  501.     /**
  502.      * Creates the node(s) for the given key value-pair. If delimiter parsing is enabled, the value string is split if
  503.      * possible, and for each single value a node is created. Otherwise only a single node is added to the section.
  504.      *
  505.      * @param sectionBuilder the section builder for adding new nodes
  506.      * @param key the key
  507.      * @param value the value string
  508.      */
  509.     private void createValueNodes(final ImmutableNode.Builder sectionBuilder, final String key, final String value) {
  510.         getListDelimiterHandler().split(value, false).forEach(v -> sectionBuilder.addChild(new ImmutableNode.Builder().name(key).value(v).create()));
  511.     }

  512.     /**
  513.      * Escapes comment characters in the given value.
  514.      *
  515.      * @param value the value to be escaped
  516.      * @return the value with comment characters escaped
  517.      */
  518.     private String escapeComments(final String value) {
  519.         final String commentChars = getCommentLeadingCharsUsedInInput();
  520.         boolean quoted = false;

  521.         for (int i = 0; i < commentChars.length(); i++) {
  522.             final char c = commentChars.charAt(i);
  523.             if (value.indexOf(c) != -1) {
  524.                 quoted = true;
  525.                 break;
  526.             }
  527.         }

  528.         if (quoted) {
  529.             return '"' + value.replace("\"", "\\\"") + '"';
  530.         }
  531.         return value;
  532.     }

  533.     /**
  534.      * Escapes the given property value before it is written. This method add quotes around the specified value if it
  535.      * contains a comment character and handles list delimiter characters.
  536.      *
  537.      * @param value the string to be escaped
  538.      */
  539.     private String escapeValue(final String value) {
  540.         return String.valueOf(getListDelimiterHandler().escape(escapeComments(value), ListDelimiterHandler.NOOP_TRANSFORMER));
  541.     }

  542.     /**
  543.      * Tries to find the index of the separator character in the given string. This method checks for the presence of
  544.      * separator characters in the given string. If multiple characters are found, the first one is assumed to be the
  545.      * correct separator. If there are quoting characters, they are taken into account, too.
  546.      *
  547.      * @param line the line to be checked
  548.      * @return the index of the separator character or -1 if none is found
  549.      */
  550.     private int findSeparator(final String line) {
  551.         int index = findSeparatorBeforeQuote(line, findFirstOccurrence(line, QUOTE_CHARACTERS));
  552.         if (index < 0) {
  553.             index = findFirstOccurrence(line, getSeparatorUsedInInput());
  554.         }
  555.         return index;
  556.     }

  557.     /**
  558.      * Gets comment leading separator used in INI reading. see {@code setCommentLeadingCharsUsedInInput} for further
  559.      * explanation
  560.      *
  561.      * @return the current separator for reading the INI input
  562.      * @since 2.5
  563.      */
  564.     public String getCommentLeadingCharsUsedInInput() {
  565.         return syncReadValue(commentCharsUsedInInput, false);
  566.     }

  567.     /**
  568.      * Creates a sub configuration for the global section of the represented INI configuration.
  569.      *
  570.      * @return the sub configuration for the global section
  571.      */
  572.     private SubnodeConfiguration getGlobalSection() {
  573.         final InMemoryNodeModel parentModel = getSubConfigurationParentModel();
  574.         final NodeSelector selector = new NodeSelector(null); // selects parent
  575.         parentModel.trackNode(selector, this);
  576.         final GlobalSectionNodeModel model = new GlobalSectionNodeModel(this, selector);
  577.         final SubnodeConfiguration sub = new SubnodeConfiguration(this, model);
  578.         initSubConfigurationForThisParent(sub);
  579.         return sub;
  580.     }

  581.     /**
  582.      * Gets a configuration with the content of the specified section. This provides an easy way of working with a single
  583.      * section only. The way this configuration is structured internally, this method is very similar to calling
  584.      * {@link HierarchicalConfiguration#configurationAt(String)} with the name of the section in question. There are the
  585.      * following differences however:
  586.      * <ul>
  587.      * <li>This method never throws an exception. If the section does not exist, it is created now. The configuration
  588.      * returned in this case is empty.</li>
  589.      * <li>If section is contained multiple times in the configuration, the configuration returned by this method is
  590.      * initialized with the first occurrence of the section. (This can only happen if {@code addProperty()} has been used in
  591.      * a way that does not conform to the storage scheme used by {@code INIConfiguration}. If used correctly, there will not
  592.      * be duplicate sections.)</li>
  593.      * <li>There is special support for the global section: Passing in <strong>null</strong> as section name returns a configuration
  594.      * with the content of the global section (which may also be empty).</li>
  595.      * </ul>
  596.      *
  597.      * @param name the name of the section in question; <strong>null</strong> represents the global section
  598.      * @return a configuration containing only the properties of the specified section
  599.      */
  600.     public SubnodeConfiguration getSection(final String name) {
  601.         if (name == null) {
  602.             return getGlobalSection();
  603.         }
  604.         try {
  605.             return (SubnodeConfiguration) configurationAt(name, true);
  606.         } catch (final ConfigurationRuntimeException iex) {
  607.             // the passed in key does not map to exactly one node
  608.             // obtain the node for the section, create it on demand
  609.             final InMemoryNodeModel parentModel = getSubConfigurationParentModel();
  610.             final NodeSelector selector = parentModel.trackChildNodeWithCreation(null, name, this);
  611.             return createSubConfigurationForTrackedNode(selector, this);
  612.         }
  613.     }

  614.     /**
  615.      * Gets a set containing the sections in this INI configuration. Note that changes to this set do not affect the
  616.      * configuration.
  617.      *
  618.      * @return a set containing the sections.
  619.      */
  620.     public Set<String> getSections() {
  621.         return syncRead(() -> {
  622.             final Set<String> sections = new LinkedHashSet<>();
  623.             boolean globalSection = false;
  624.             boolean inSection = false;
  625.             for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) {
  626.                 if (isSectionNode(node)) {
  627.                     inSection = true;
  628.                     sections.add(node.getNodeName());
  629.                 } else if (!inSection && !globalSection) {
  630.                     globalSection = true;
  631.                     sections.add(null);
  632.                 }
  633.             }
  634.             return sections;
  635.         }, false);
  636.     }

  637.     /**
  638.      * Gets separator used in INI reading. see {@code setSeparatorUsedInInput} for further explanation
  639.      *
  640.      * @return the current separator for reading the INI input
  641.      * @since 2.5
  642.      */
  643.     public String getSeparatorUsedInInput() {
  644.         return syncReadValue(separatorUsedInInput, false);
  645.     }

  646.     /**
  647.      * Gets separator used in INI output. see {@code setSeparatorUsedInOutput} for further explanation
  648.      *
  649.      * @return the current separator for writing the INI output
  650.      * @since 2.2
  651.      */
  652.     public String getSeparatorUsedInOutput() {
  653.         return syncReadValue(separatorUsedInOutput, false);
  654.     }

  655.     /**
  656.      * Tests whether the specified character is a comment character.
  657.      *
  658.      * @param c the character
  659.      * @return a flag whether this character starts a comment
  660.      */
  661.     private boolean isCommentChar(final char c) {
  662.         return getCommentLeadingCharsUsedInInput().indexOf(c) >= 0;
  663.     }

  664.     /**
  665.      * Determine if the given line is a comment line.
  666.      *
  667.      * @param line The line to check.
  668.      * @return true if the line is empty or starts with one of the comment characters
  669.      */
  670.     protected boolean isCommentLine(final String line) {
  671.         if (line == null) {
  672.             return false;
  673.         }
  674.         // blank lines are also treated as comment lines
  675.         return line.isEmpty() || getCommentLeadingCharsUsedInInput().indexOf(line.charAt(0)) >= 0;
  676.     }

  677.     /**
  678.      * Determine if the given line is a section.
  679.      *
  680.      * @param line The line to check.
  681.      * @return true if the line contains a section
  682.      */
  683.     protected boolean isSectionLine(final String line) {
  684.         if (line == null) {
  685.             return false;
  686.         }
  687.         return sectionInLineCommentsAllowed ? isNonStrictSection(line) : isStrictSection(line);
  688.     }

  689.     /**
  690.      * Tests whether the specified string contains a line continuation marker after the specified position. This method
  691.      * parses the string to remove a comment that might be present. Then it checks whether a line continuation marker can be
  692.      * found at the end.
  693.      *
  694.      * @param line the line to check
  695.      * @param pos the start position
  696.      * @return a flag whether this line continues
  697.      */
  698.     private boolean lineContinues(final String line, final int pos) {
  699.         final String s;

  700.         if (pos >= line.length()) {
  701.             s = line;
  702.         } else {
  703.             int end = pos;
  704.             while (end < line.length() && !isCommentChar(line.charAt(end))) {
  705.                 end++;
  706.             }
  707.             s = line.substring(pos, end);
  708.         }

  709.         return lineContinues(s);
  710.     }

  711.     /**
  712.      * Parse the value to remove the quotes and ignoring the comment. Example:
  713.      *
  714.      * <pre>
  715.      * &quot;value&quot; ; comment -&gt; value
  716.      * </pre>
  717.      *
  718.      * <pre>
  719.      * 'value' ; comment -&gt; value
  720.      * </pre>
  721.      *
  722.      * Note that a comment character is only recognized if there is at least one whitespace character before it. So it can
  723.      * appear in the property value, for example:
  724.      *
  725.      * <pre>
  726.      * C:\\Windows;C:\\Windows\\system32
  727.      * </pre>
  728.      *
  729.      * @param val the value to be parsed
  730.      * @param reader the reader (needed if multiple lines have to be read)
  731.      * @throws IOException if an IO error occurs
  732.      */
  733.     private String parseValue(final String val, final BufferedReader reader) throws IOException {
  734.         final StringBuilder propertyValue = new StringBuilder();
  735.         boolean lineContinues;
  736.         String value = val.trim();

  737.         do {
  738.             final boolean quoted = value.startsWith("\"") || value.startsWith("'");
  739.             boolean stop = false;
  740.             boolean escape = false;

  741.             final char quote = quoted ? value.charAt(0) : 0;

  742.             int i = quoted ? 1 : 0;

  743.             final StringBuilder result = new StringBuilder();
  744.             char lastChar = 0;
  745.             while (i < value.length() && !stop) {
  746.                 final char c = value.charAt(i);

  747.                 if (quoted) {
  748.                     if ('\\' == c && !escape) {
  749.                         escape = true;
  750.                     } else if (!escape && quote == c) {
  751.                         stop = true;
  752.                     } else {
  753.                         if (escape && quote == c) {
  754.                             escape = false;
  755.                         } else if (escape) {
  756.                             escape = false;
  757.                             result.append('\\');
  758.                         }
  759.                         result.append(c);
  760.                     }
  761.                 } else if (isCommentChar(c) && Character.isWhitespace(lastChar)) {
  762.                     stop = true;
  763.                 } else {
  764.                     result.append(c);
  765.                 }

  766.                 i++;
  767.                 lastChar = c;
  768.             }

  769.             String v = result.toString();
  770.             if (!quoted) {
  771.                 v = v.trim();
  772.                 lineContinues = lineContinues(v);
  773.                 if (lineContinues) {
  774.                     // remove trailing "\"
  775.                     v = v.substring(0, v.length() - 1).trim();
  776.                 }
  777.             } else {
  778.                 lineContinues = lineContinues(value, i);
  779.             }
  780.             propertyValue.append(v);

  781.             if (lineContinues) {
  782.                 propertyValue.append(LINE_SEPARATOR);
  783.                 value = reader.readLine();
  784.             }
  785.         } while (lineContinues && value != null);

  786.         return propertyValue.toString();
  787.     }

  788.     /**
  789.      * Load the configuration from the given reader. Note that the {@code clear()} method is not called so the configuration
  790.      * read in will be merged with the current configuration.
  791.      *
  792.      * @param in the reader to read the configuration from.
  793.      * @throws ConfigurationException If an error occurs while reading the configuration
  794.      * @throws IOException if an I/O error occurs.
  795.      */
  796.     @Override
  797.     public void read(final Reader in) throws ConfigurationException, IOException {
  798.         final BufferedReader bufferedReader = new BufferedReader(in);
  799.         final Map<String, ImmutableNode.Builder> sectionBuilders = new LinkedHashMap<>();
  800.         final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();

  801.         createNodeBuilders(bufferedReader, rootBuilder, sectionBuilders);
  802.         final ImmutableNode rootNode = createNewRootNode(rootBuilder, sectionBuilders);
  803.         addNodes(null, rootNode.getChildren());
  804.     }

  805.     /**
  806.      * Allows setting the leading comment separator which is used in reading an INI file
  807.      *
  808.      * @param separator String of the new separator for INI reading
  809.      * @since 2.5
  810.      */
  811.     public void setCommentLeadingCharsUsedInInput(final String separator) {
  812.         beginRead(false);
  813.         try {
  814.             this.commentCharsUsedInInput = separator;
  815.         } finally {
  816.             endRead();
  817.         }
  818.     }

  819.     /**
  820.      * Allows setting the key and value separator which is used in reading an INI file
  821.      *
  822.      * @param separator String of the new separator for INI reading
  823.      * @since 2.5
  824.      */
  825.     public void setSeparatorUsedInInput(final String separator) {
  826.         beginRead(false);
  827.         try {
  828.             this.separatorUsedInInput = separator;
  829.         } finally {
  830.             endRead();
  831.         }
  832.     }

  833.     /**
  834.      * Allows setting the key and value separator which is used for the creation of the resulting INI output
  835.      *
  836.      * @param separator String of the new separator for INI output
  837.      * @since 2.2
  838.      */
  839.     public void setSeparatorUsedInOutput(final String separator) {
  840.         syncWrite(() -> this.separatorUsedInOutput = separator, false);
  841.     }

  842.     /**
  843.      * Save the configuration to the specified writer.
  844.      *
  845.      * @param writer   The writer to save the configuration to.
  846.      * @throws ConfigurationException If an error occurs while writing the configuration
  847.      * @throws IOException if an I/O error occurs.
  848.      */
  849.     @Override
  850.     public void write(final Writer writer) throws ConfigurationException, IOException {
  851.         syncRead(() -> {
  852.             final PrintWriter out = new PrintWriter(writer);
  853.             boolean first = true;
  854.             final String separator = getSeparatorUsedInOutput();
  855.             for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) {
  856.                 if (isSectionNode(node)) {
  857.                     if (!first) {
  858.                         out.println();
  859.                     }
  860.                     out.print("[");
  861.                     out.print(node.getNodeName());
  862.                     out.print("]");
  863.                     out.println();

  864.                     node.forEach(child -> writeProperty(out, child.getNodeName(), child.getValue(), separator));
  865.                 } else {
  866.                     writeProperty(out, node.getNodeName(), node.getValue(), separator);
  867.                 }
  868.                 first = false;
  869.             }
  870.             out.println();
  871.             out.flush();

  872.         }, false);
  873.     }

  874.     /**
  875.      * Writes data about a property into the given stream.
  876.      *
  877.      * @param out the output stream
  878.      * @param key the key
  879.      * @param value the value
  880.      */
  881.     private void writeProperty(final PrintWriter out, final String key, final Object value, final String separator) {
  882.         out.print(key);
  883.         out.print(separator);
  884.         out.print(escapeValue(value.toString()));
  885.         out.println();
  886.     }
  887. }