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
018package org.apache.commons.configuration;
019
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.IOException;
023import java.io.PrintWriter;
024import java.io.Reader;
025import java.io.Writer;
026import java.net.URL;
027import java.util.Collection;
028import java.util.Iterator;
029import java.util.Set;
030import 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
171public 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}