1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 import org.apache.commons.lang.StringUtils;
33
34 /***
35 * <p>
36 * An initialization or ini file is a configuration file tpically found on
37 * Microsoft's Windows operating system and contains data for Windows based
38 * applications.
39 * </p>
40 *
41 * <p>
42 * Although popularized by Windows, ini files can be used on any system or
43 * platform due to the fact that they are merely text files that can easily be
44 * parsed and modified by both humans and computers.
45 * </p>
46 *
47 * <p>
48 * A typcial ini file could look something like:
49 * </p>
50 * <code>
51 * [section1]<br>
52 * ; this is a comment!<br>
53 * var1 = foo<br>
54 * var2 = bar<br>
55 *<br>
56 * [section2]<br>
57 * var1 = doo<br>
58 * </code>
59 *
60 * <p>
61 * The format of ini files is fairly straight forward and is composed of three
62 * components:<br>
63 * <ul>
64 * <li><b>Sections:</b> Ini files are split into sections, each section
65 * starting with a section declaration. A section declaration starts with a '['
66 * and ends with a ']'. Sections occur on one line only.</li>
67 * <li><b>Parameters:</b> Items in a section are known as parameters.
68 * Parameters have a typical <code>key = value</code> format.</li>
69 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.
70 * </li>
71 * </ul>
72 * </p>
73 *
74 * <p>
75 * There are various implementations of the ini file format by various vendors
76 * which has caused a number of differences to appear. As far as possible this
77 * configuration tries to be lenient and support most of the differences.
78 * </p>
79 *
80 * <p>
81 * Some of the differences supported are as follows:
82 * <ul>
83 * <li><b>Comments:</b> The '#' character is also accepted as a comment
84 * signifier.</li>
85 * <li><b>Key value separtor:</b> The ':' character is also accepted in place
86 * of '=' to separate keys and values in parameters, for example
87 * <code>var1 : foo</code>.</li>
88 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed ,
89 * this configuration does however support it. In the event of a duplicate
90 * section, the two section's values are merged.</li>
91 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
92 * allowed if they are in two different sections, thus they are local to
93 * sections; this configuration simply merges duplicates; if a section has a
94 * duplicate parameter the values are then added to the key as a list. </li>
95 * </ul>
96 * </p>
97 * <p>
98 * Global parameters are also allowed; any parameters declared before a section
99 * is declared are added to a global section. It is important to note that this
100 * global section does not have a name.
101 * </p>
102 * <p>
103 * In all instances, a parameter's key is prepended with its section name and a
104 * '.' (period). Thus a parameter named "var1" in "section1" will have the key
105 * <code>section1.var1</code> in this configuration. Thus, a section's
106 * parameters can easily be retrieved using the <code>subset</code> method
107 * using the section name as the prefix.
108 * </p>
109 * <p>
110 * <h3>Implementation Details:</h3>
111 * Consider the following ini file:<br>
112 * <code>
113 * default = ok<br>
114 * <br>
115 * [section1]<br>
116 * var1 = foo<br>
117 * var2 = doodle<br>
118 * <br>
119 * [section2]<br>
120 * ; a comment<br>
121 * var1 = baz<br>
122 * var2 = shoodle<br>
123 * bad =<br>
124 * = worse<br>
125 * <br>
126 * [section3]<br>
127 * # another comment<br>
128 * var1 : foo<br>
129 * var2 : bar<br>
130 * var5 : test1<br>
131 * <br>
132 * [section3]<br>
133 * var3 = foo<br>
134 * var4 = bar<br>
135 * var5 = test2<br>
136 * </code>
137 * </p>
138 * <p>
139 * This ini file will be parsed without error. Note:
140 * <ul>
141 * <li>The parameter named "default" is added to the global section, it's value
142 * is accessed simply using <code>getProperty("default")</code>.</li>
143 * <li>Section 1's parameters can be accessed using
144 * <code>getProperty("section1.var1")</code>.</li>
145 * <li>The parameter named "bad" simply adds the parameter with an empty value.
146 * </li>
147 * <li>The empty key with value "= worse" is added using an empty key. This key
148 * is still added to section 2 and the value can be accessed using
149 * <code>getProperty("section2.")</code>, notice the period '.' following the
150 * section name.</li>
151 * <li>Section three uses both '=' and ':' to separate keys and values.</li>
152 * <li>Section 3 has a duplicate key named "var5". The value for this key is
153 * [test1, test2], and is represented as a List.</li>
154 * </ul>
155 * </p>
156 * <p>
157 * The set of sections in this configuration can be retrieved using the
158 * <code>getSections</code> method.
159 * </p>
160 * <p>
161 * <em>Note:</em>Configuration objects of this type can be read concurrently
162 * by multiple threads. However if one of these threads modifies the object,
163 * synchronization has to be performed manually.
164 * </p>
165 *
166 * @author Trevor Miller
167 * @version $Id: INIConfiguration.java 589380 2007-10-28 16:37:35Z oheger $
168 * @since 1.4
169 */
170 public class INIConfiguration extends AbstractFileConfiguration
171 {
172 /***
173 * The characters that signal the start of a comment line.
174 */
175 protected static final String COMMENT_CHARS = "#;";
176
177 /***
178 * The characters used to separate keys from values.
179 */
180 protected static final String SEPARATOR_CHARS = "=:";
181
182 /***
183 * Create a new empty INI Configuration.
184 */
185 public INIConfiguration()
186 {
187 super();
188 }
189
190 /***
191 * Create and load the ini configuration from the given file.
192 *
193 * @param filename The name pr path of the ini file to load.
194 * @throws ConfigurationException If an error occurs while loading the file
195 */
196 public INIConfiguration(String filename) throws ConfigurationException
197 {
198 super(filename);
199 }
200
201 /***
202 * Create and load the ini configuration from the given file.
203 *
204 * @param file The ini file to load.
205 * @throws ConfigurationException If an error occurs while loading the file
206 */
207 public INIConfiguration(File file) throws ConfigurationException
208 {
209 super(file);
210 }
211
212 /***
213 * Create and load the ini configuration from the given url.
214 *
215 * @param url The url of the ini file to load.
216 * @throws ConfigurationException If an error occurs while loading the file
217 */
218 public INIConfiguration(URL url) throws ConfigurationException
219 {
220 super(url);
221 }
222
223 /***
224 * Save the configuration to the specified writer.
225 *
226 * @param writer - The writer to save the configuration to.
227 * @throws ConfigurationException If an error occurs while writing the
228 * configuration
229 */
230 public void save(Writer writer) throws ConfigurationException
231 {
232 PrintWriter out = new PrintWriter(writer);
233 Iterator it = getSections().iterator();
234 while (it.hasNext())
235 {
236 String section = (String) it.next();
237 out.print("[");
238 out.print(section);
239 out.print("]");
240 out.println();
241
242 Configuration subset = subset(section);
243 Iterator keys = subset.getKeys();
244 while (keys.hasNext())
245 {
246 String key = (String) keys.next();
247 Object value = subset.getProperty(key);
248 if (value instanceof Collection)
249 {
250 Iterator values = ((Collection) value).iterator();
251 while (values.hasNext())
252 {
253 value = (Object) values.next();
254 out.print(key);
255 out.print(" = ");
256 out.print(formatValue(value.toString()));
257 out.println();
258 }
259 }
260 else
261 {
262 out.print(key);
263 out.print(" = ");
264 out.print(formatValue(value.toString()));
265 out.println();
266 }
267 }
268
269 out.println();
270 }
271
272 out.flush();
273 }
274
275 /***
276 * Load the configuration from the given reader. Note that the
277 * <code>clear</code> method is not called so the configuration read in
278 * will be merged with the current configuration.
279 *
280 * @param reader The reader to read the configuration from.
281 * @throws ConfigurationException If an error occurs while reading the
282 * configuration
283 */
284 public void load(Reader reader) throws ConfigurationException
285 {
286 try
287 {
288 BufferedReader bufferedReader = new BufferedReader(reader);
289 String line = bufferedReader.readLine();
290 String section = "";
291 while (line != null)
292 {
293 line = line.trim();
294 if (!isCommentLine(line))
295 {
296 if (isSectionLine(line))
297 {
298 section = line.substring(1, line.length() - 1) + ".";
299 }
300 else
301 {
302 String key = "";
303 String value = "";
304 int index = line.indexOf("=");
305 if (index >= 0)
306 {
307 key = section + line.substring(0, index);
308 value = parseValue(line.substring(index + 1));
309 }
310 else
311 {
312 index = line.indexOf(":");
313 if (index >= 0)
314 {
315 key = section + line.substring(0, index);
316 value = parseValue(line.substring(index + 1));
317 }
318 else
319 {
320 key = section + line;
321 }
322 }
323 addProperty(key.trim(), value);
324 }
325 }
326 line = bufferedReader.readLine();
327 }
328 }
329 catch (IOException ioe)
330 {
331 throw new ConfigurationException(ioe.getMessage());
332 }
333 }
334
335 /***
336 * Parse the value to remove the quotes and ignoring the comment.
337 * Example:
338 *
339 * <pre>"value" ; comment -> value</pre>
340 *
341 * <pre>'value' ; comment -> value</pre>
342 *
343 * @param value
344 */
345 private String parseValue(String value)
346 {
347 value = value.trim();
348
349 boolean quoted = value.startsWith("\"") || value.startsWith("'");
350 boolean stop = false;
351 boolean escape = false;
352
353 char quote = quoted ? value.charAt(0) : 0;
354
355 int i = quoted ? 1 : 0;
356
357 StringBuffer result = new StringBuffer();
358 while (i < value.length() && !stop)
359 {
360 char c = value.charAt(i);
361
362 if (quoted)
363 {
364 if ('//' == c && !escape)
365 {
366 escape = true;
367 }
368 else if (!escape && quote == c)
369 {
370 stop = true;
371 }
372 else if (escape && quote == c)
373 {
374 escape = false;
375 result.append(c);
376 }
377 else
378 {
379 if (escape)
380 {
381 escape = false;
382 result.append('//');
383 }
384
385 result.append(c);
386 }
387 }
388 else
389 {
390 if (COMMENT_CHARS.indexOf(c) == -1)
391 {
392 result.append(c);
393 }
394 else
395 {
396 stop = true;
397 }
398 }
399
400 i++;
401 }
402
403 return result.toString().trim();
404 }
405
406 /***
407 * Add quotes around the specified value if it contains a comment character.
408 */
409 private String formatValue(String value)
410 {
411 boolean quoted = false;
412
413 for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
414 {
415 char c = COMMENT_CHARS.charAt(i);
416 if (value.indexOf(c) != -1)
417 {
418 quoted = true;
419 }
420 }
421
422 if (quoted)
423 {
424 return '"' + StringUtils.replace(value, "\"", "//\"") + '"';
425 }
426 else
427 {
428 return value;
429 }
430 }
431
432 /***
433 * Determine if the given line is a comment line.
434 *
435 * @param line The line to check.
436 * @return true if the line is empty or starts with one of the comment
437 * characters
438 */
439 protected boolean isCommentLine(String line)
440 {
441 if (line == null)
442 {
443 return false;
444 }
445
446 return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
447 }
448
449 /***
450 * Determine if the given line is a section.
451 *
452 * @param line The line to check.
453 * @return true if the line contains a secion
454 */
455 protected boolean isSectionLine(String line)
456 {
457 if (line == null)
458 {
459 return false;
460 }
461 return line.startsWith("[") && line.endsWith("]");
462 }
463
464 /***
465 * Return a set containing the sections in this ini configuration. Note that
466 * changes to this set do not affect the configuration.
467 *
468 * @return a set containing the sections.
469 */
470 public Set getSections()
471 {
472 Set sections = new TreeSet();
473
474 Iterator keys = getKeys();
475 while (keys.hasNext())
476 {
477 String key = (String) keys.next();
478 int index = key.indexOf(".");
479 if (index >= 0)
480 {
481 sections.add(key.substring(0, index));
482 }
483 }
484
485 return sections;
486 }
487 }