    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     */
    017
    018    package org.apache.commons.configuration;
    019
    020    import java.io.BufferedReader;
    021    import java.io.File;
    022    import java.io.IOException;
    023    import java.io.PrintWriter;
    024    import java.io.Reader;
    025    import java.io.Writer;
    026    import java.net.URL;
    027    import java.util.Collection;
    028    import java.util.Iterator;
    029    import java.util.Set;
    030    import java.util.TreeSet;
    031
    032    /**
    033     * <p>
    034     * An initialization or ini file is a configuration file typically found on
    035     * Microsoft's Windows operating system and contains data for Windows based
    036     * applications.
    037     * </p>
    038     *
    039     * <p>
    040     * Although popularized by Windows, ini files can be used on any system or
    041     * platform due to the fact that they are merely text files that can easily be
    042     * parsed and modified by both humans and computers.
    043     * </p>
    044     *
    045     * <p>
    046     * A typical ini file could look something like:
    047     * </p>
    048     * <pre>
    049     * [section1]
    050     * ; this is a comment!
    051     * var1 = foo
    052     * var2 = bar
    053     *
    054     * [section2]
    055     * var1 = doo
    056     * </pre>
    057     *
    058     * <p>
    059     * The format of ini files is fairly straight forward and is composed of three
    060     * components:<br>
    061     * <ul>
    062     * <li><b>Sections:</b> Ini files are split into sections, each section
    063     * starting with a section declaration. A section declaration starts with a '['
    064     * and ends with a ']'. Sections occur on one line only.</li>
    065     * <li><b>Parameters:</b> Items in a section are known as parameters.
    066     * Parameters have a typical {@code key = value} format.</li>
    067     * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.
    068     * </li>
    069     * </ul>
    070     * </p>
    071     *
    072     * <p>
    073     * There are various implementations of the ini file format by various vendors
    074     * which has caused a number of differences to appear. As far as possible this
    075     * configuration tries to be lenient and support most of the differences.
    076     * </p>
    077     *
    078     * <p>
    079     * Some of the differences supported are as follows:
    080     * <ul>
    081     * <li><b>Comments:</b> The '#' character is also accepted as a comment
    082     * signifier.</li>
    083     * <li><b>Key value separtor:</b> The ':' character is also accepted in place
    084     * of '=' to separate keys and values in parameters, for example
    085     * {@code var1 : foo}.</li>
    086     * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed ,
    087     * this configuration does however support it. In the event of a duplicate
    088     * section, the two section's values are merged.</li>
    089     * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
    090     * allowed if they are in two different sections, thus they are local to
    091     * sections; this configuration simply merges duplicates; if a section has a
    092     * duplicate parameter the values are then added to the key as a list. </li>
    093     * </ul>
    094     * </p>
    095     * <p>
    096     * Global parameters are also allowed; any parameters declared before a section
    097     * is declared are added to a global section. It is important to note that this
    098     * global section does not have a name.
    099     * </p>
    100     * <p>
    101     * In all instances, a parameter's key is prepended with its section name and a
    102     * '.' (period). Thus a parameter named "var1" in "section1" will have the key
    103     * {@code section1.var1} in this configuration. Thus, a section's
    104     * parameters can easily be retrieved using the {@code subset} method
    105     * using the section name as the prefix.
    106     * </p>
    107     * <p>
    108     * <h3>Implementation Details:</h3>
    109     * Consider the following ini file:<br>
    110     * <pre>
    111     *  default = ok
    112     *
    113     *  [section1]
    114     *  var1 = foo
    115     *  var2 = doodle
    116     *
    117     *  [section2]
    118     *  ; a comment
    119     *  var1 = baz
    120     *  var2 = shoodle
    121     *  bad =
    122     *  = worse
    123     *
    124     *  [section3]
    125     *  # another comment
    126     *  var1 : foo
    127     *  var2 : bar
    128     *  var5 : test1
    129     *
    130     *  [section3]
    131     *  var3 = foo
    132     *  var4 = bar
    133     *  var5 = test2
    134     *  </pre>
    135     * </p>
    136     * <p>
    137     * This ini file will be parsed without error. Note:
    138     * <ul>
    139     * <li>The parameter named "default" is added to the global section, it's value
    140     * is accessed simply using {@code getProperty("default")}.</li>
    141     * <li>Section 1's parameters can be accessed using
    142     * {@code getProperty("section1.var1")}.</li>
    143     * <li>The parameter named "bad" simply adds the parameter with an empty value.
    144     * </li>
    145     * <li>The empty key with value "= worse" is added using an empty key. This key
    146     * is still added to section 2 and the value can be accessed using
    147     * {@code getProperty("section2.")}, notice the period '.' following the
    148     * 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
    151     * [test1, test2], and is represented as a List.</li>
    152     * </ul>
    153     * </p>
    154     * <p>
    155     * The set of sections in this configuration can be retrieved using the
    156     * {@code getSections} method.
    157     * </p>
    158     * <p>
    159     * <em>Note:</em> Configuration objects of this type can be read concurrently
    160     * by multiple threads. However if one of these threads modifies the object,
    161     * synchronization has to be performed manually.
    162     * </p>
    163     *
    164     * @author Trevor Miller
    165     * @version $Id: INIConfiguration.java 1210003 2011-12-03 20:54:46Z oheger $
    166     * @since 1.4
    167     * @deprecated This class has been replaced by HierarchicalINIConfiguration,
    168     * which provides a superset of the functionality offered by this class.
    169     */
    170    @Deprecated
    171    public class INIConfiguration extends AbstractFileConfiguration
    172    {
    173        /**
    174         * The characters that signal the start of a comment line.
    175         */
    176        protected static final String COMMENT_CHARS = "#;";
    177
    178        /**
    179         * The characters used to separate keys from values.
    180         */
    181        protected static final String SEPARATOR_CHARS = "=:";
    182
    183        /**
    184         * Create a new empty INI Configuration.
    185         */
    186        public INIConfiguration()
    187        {
    188            super();
    189        }
    190
    191        /**
    192         * Create and load the ini configuration from the given file.
    193         *
    194         * @param filename The name pr path of the ini file to load.
    195         * @throws ConfigurationException If an error occurs while loading the file
    196         */
    197        public INIConfiguration(String filename) throws ConfigurationException
    198        {
    199            super(filename);
    200        }
    201
    202        /**
    203         * Create and load the ini configuration from the given file.
    204         *
    205         * @param file The ini file to load.
    206         * @throws ConfigurationException If an error occurs while loading the file
    207         */
    208        public INIConfiguration(File file) throws ConfigurationException
    209        {
    210            super(file);
    211        }
    212
    213        /**
    214         * Create and load the ini configuration from the given url.
    215         *
    216         * @param url The url of the ini file to load.
    217         * @throws ConfigurationException If an error occurs while loading the file
    218         */
    219        public INIConfiguration(URL url) throws ConfigurationException
    220        {
    221            super(url);
    222        }
    223
    224        /**
    225         * Save the configuration to the specified writer.
    226         *
    227         * @param writer - The writer to save the configuration to.
    228         * @throws ConfigurationException If an error occurs while writing the
    229         * configuration
    230         */
    231        public void save(Writer writer) throws ConfigurationException
    232        {
    233            PrintWriter out = new PrintWriter(writer);
    234            Iterator<String> it = getSections().iterator();
    235            while (it.hasNext())
    236            {
    237                String section = it.next();
    238                out.print("[");
    239                out.print(section);
    240                out.print("]");
    241                out.println();
    242
    243                Configuration subset = subset(section);
    244                Iterator<String> keys = subset.getKeys();
    245                while (keys.hasNext())
    246                {
    247                    String key = keys.next();
    248                    Object value = subset.getProperty(key);
    249                    if (value instanceof Collection)
    250                    {
    251                        Iterator<?> values = ((Collection<?>) value).iterator();
    252                        while (values.hasNext())
    253                        {
    254                            value = values.next();
    255                            out.print(key);
    256                            out.print(" = ");
    257                            out.print(formatValue(value.toString()));
    258                            out.println();
    259                        }
    260                    }
    261                    else
    262                    {
    263                        out.print(key);
    264                        out.print(" = ");
    265                        out.print(formatValue(value.toString()));
    266                        out.println();
    267                    }
    268                }
    269
    270                out.println();
    271            }
    272
    273            out.flush();
    274        }
    275
    276        /**
    277         * Load the configuration from the given reader. Note that the
    278         * {@code clear()} method is not called so the configuration read in
    279         * will be merged with the current configuration.
    280         *
    281         * @param reader The reader to read the configuration from.
    282         * @throws ConfigurationException If an error occurs while reading the
    283         * configuration
    284         */
    285        public void load(Reader reader) throws ConfigurationException
    286        {
    287            try
    288            {
    289                BufferedReader bufferedReader = new BufferedReader(reader);
    290                String line = bufferedReader.readLine();
    291                String section = "";
    292                while (line != null)
    293                {
    294                    line = line.trim();
    295                    if (!isCommentLine(line))
    296                    {
    297                        if (isSectionLine(line))
    298                        {
    299                            section = line.substring(1, line.length() - 1) + ".";
    300                        }
    301                        else
    302                        {
    303                            String key = "";
    304                            String value = "";
    305                            int index = line.indexOf("=");
    306                            if (index >= 0)
    307                            {
    308                                key = section + line.substring(0, index);
    309                                value = parseValue(line.substring(index + 1));
    310                            }
    311                            else
    312                            {
    313                                index = line.indexOf(":");
    314                                if (index >= 0)
    315                                {
    316                                    key = section + line.substring(0, index);
    317                                    value = parseValue(line.substring(index + 1));
    318                                }
    319                                else
    320                                {
    321                                    key = section + line;
    322                                }
    323                            }
    324                            addProperty(key.trim(), value);
    325                        }
    326                    }
    327                    line = bufferedReader.readLine();
    328                }
    329            }
    330            catch (IOException e)
    331            {
    332                throw new ConfigurationException("Unable to load the configuration", e);
    333            }
    334        }
    335
    336        /**
    337         * Parse the value to remove the quotes and ignoring the comment.
    338         * Example:
    339         *
    340         * <pre>"value" ; comment -> value</pre>
    341         *
    342         * <pre>'value' ; comment -> value</pre>
    343         *
    344         * @param value
    345         */
    346        private String parseValue(String value)
    347        {
    348            value = value.trim();
    349
    350            boolean quoted = value.startsWith("\"") || value.startsWith("'");
    351            boolean stop = false;
    352            boolean escape = false;
    353
    354            char quote = quoted ? value.charAt(0) : 0;
    355
    356            int i = quoted ? 1 : 0;
    357
    358            StringBuilder result = new StringBuilder();
    359            while (i < value.length() && !stop)
    360            {
    361                char c = value.charAt(i);
    362
    363                if (quoted)
    364                {
    365                    if ('\\' == c && !escape)
    366                    {
    367                        escape = true;
    368                    }
    369                    else if (!escape && quote == c)
    370                    {
    371                        stop = true;
    372                    }
    373                    else if (escape && quote == c)
    374                    {
    375                        escape = false;
    376                        result.append(c);
    377                    }
    378                    else
    379                    {
    380                        if (escape)
    381                        {
    382                            escape = false;
    383                            result.append('\\');
    384                        }
    385
    386                        result.append(c);
    387                    }
    388                }
    389                else
    390                {
    391                    if (COMMENT_CHARS.indexOf(c) == -1)
    392                    {
    393                        result.append(c);
    394                    }
    395                    else
    396                    {
    397                        stop = true;
    398                    }
    399                }
    400
    401                i++;
    402            }
    403
    404            String v = result.toString();
    405            if (!quoted)
    406            {
    407                v = v.trim();
    408            }
    409            return v;
    410        }
    411
    412        /**
    413         * Add quotes around the specified value if it contains a comment character.
    414         */
    415        private String formatValue(String value)
    416        {
    417            boolean quoted = false;
    418
    419            for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
    420            {
    421                char c = COMMENT_CHARS.charAt(i);
    422                if (value.indexOf(c) != -1)
    423                {
    424                    quoted = true;
    425                }
    426            }
    427
    428            if (quoted)
    429            {
    430                return '"' + value.replaceAll("\"", "\\\\\\\"") + '"';
    431            }
    432            else
    433            {
    434                return value;
    435            }
    436        }
    437
    438        /**
    439         * Determine if the given line is a comment line.
    440         *
    441         * @param line The line to check.
    442         * @return true if the line is empty or starts with one of the comment
    443         * characters
    444         */
    445        protected boolean isCommentLine(String line)
    446        {
    447            if (line == null)
    448            {
    449                return false;
    450            }
    451            // blank lines are also treated as comment lines
    452            return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
    453        }
    454
    455        /**
    456         * Determine if the given line is a section.
    457         *
    458         * @param line The line to check.
    459         * @return true if the line contains a secion
    460         */
    461        protected boolean isSectionLine(String line)
    462        {
    463            if (line == null)
    464            {
    465                return false;
    466            }
    467            return line.startsWith("[") && line.endsWith("]");
    468        }
    469
    470        /**
    471         * Return a set containing the sections in this ini configuration. Note that
    472         * changes to this set do not affect the configuration.
    473         *
    474         * @return a set containing the sections.
    475         */
    476        public Set<String> getSections()
    477        {
    478            Set<String> sections = new TreeSet<String>();
    479
    480            Iterator<String> keys = getKeys();
    481            while (keys.hasNext())
    482            {
    483                String key = keys.next();
    484                int index = key.indexOf(".");
    485                if (index >= 0)
    486                {
    487                    sections.add(key.substring(0, index));
    488                }
    489            }
    490
    491            return sections;
    492        }
    493    }