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    }