View Javadoc

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  
18  package org.apache.commons.configuration;
19  
20  import java.io.BufferedReader;
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.PrintWriter;
24  import java.io.Reader;
25  import java.io.Writer;
26  import java.net.URL;
27  import java.util.Collection;
28  import java.util.Iterator;
29  import java.util.Set;
30  import java.util.TreeSet;
31  
32  /**
33   * <p>
34   * An initialization or ini file is a configuration file typically found on
35   * Microsoft's Windows operating system and contains data for Windows based
36   * applications.
37   * </p>
38   *
39   * <p>
40   * Although popularized by Windows, ini files can be used on any system or
41   * platform due to the fact that they are merely text files that can easily be
42   * parsed and modified by both humans and computers.
43   * </p>
44   *
45   * <p>
46   * A typical ini file could look something like:
47   * </p>
48   * <pre>
49   * [section1]
50   * ; this is a comment!
51   * var1 = foo
52   * var2 = bar
53   *
54   * [section2]
55   * var1 = doo
56   * </pre>
57   *
58   * <p>
59   * The format of ini files is fairly straight forward and is composed of three
60   * components:<br>
61   * <ul>
62   * <li><b>Sections:</b> Ini files are split into sections, each section
63   * starting with a section declaration. A section declaration starts with a '['
64   * and ends with a ']'. Sections occur on one line only.</li>
65   * <li><b>Parameters:</b> Items in a section are known as parameters.
66   * Parameters have a typical {@code key = value} format.</li>
67   * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.
68   * </li>
69   * </ul>
70   * </p>
71   *
72   * <p>
73   * There are various implementations of the ini file format by various vendors
74   * which has caused a number of differences to appear. As far as possible this
75   * configuration tries to be lenient and support most of the differences.
76   * </p>
77   *
78   * <p>
79   * Some of the differences supported are as follows:
80   * <ul>
81   * <li><b>Comments:</b> The '#' character is also accepted as a comment
82   * signifier.</li>
83   * <li><b>Key value separtor:</b> The ':' character is also accepted in place
84   * of '=' to separate keys and values in parameters, for example
85   * {@code var1 : foo}.</li>
86   * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed ,
87   * this configuration does however support it. In the event of a duplicate
88   * section, the two section's values are merged.</li>
89   * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
90   * allowed if they are in two different sections, thus they are local to
91   * sections; this configuration simply merges duplicates; if a section has a
92   * duplicate parameter the values are then added to the key as a list. </li>
93   * </ul>
94   * </p>
95   * <p>
96   * Global parameters are also allowed; any parameters declared before a section
97   * is declared are added to a global section. It is important to note that this
98   * global section does not have a name.
99   * </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 }