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 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 620146 2008-02-09 16:33:45Z 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 String v = result.toString();
404 if(!quoted)
405 {
406 v = v.trim();
407 }
408 return v;
409 }
410
411 /**
412 * Add quotes around the specified value if it contains a comment character.
413 */
414 private String formatValue(String value)
415 {
416 boolean quoted = false;
417
418 for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
419 {
420 char c = COMMENT_CHARS.charAt(i);
421 if (value.indexOf(c) != -1)
422 {
423 quoted = true;
424 }
425 }
426
427 if (quoted)
428 {
429 return '"' + StringUtils.replace(value, "\"", "\\\"") + '"';
430 }
431 else
432 {
433 return value;
434 }
435 }
436
437 /**
438 * Determine if the given line is a comment line.
439 *
440 * @param line The line to check.
441 * @return true if the line is empty or starts with one of the comment
442 * characters
443 */
444 protected boolean isCommentLine(String line)
445 {
446 if (line == null)
447 {
448 return false;
449 }
450 // blank lines are also treated as comment lines
451 return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
452 }
453
454 /**
455 * Determine if the given line is a section.
456 *
457 * @param line The line to check.
458 * @return true if the line contains a secion
459 */
460 protected boolean isSectionLine(String line)
461 {
462 if (line == null)
463 {
464 return false;
465 }
466 return line.startsWith("[") && line.endsWith("]");
467 }
468
469 /**
470 * Return a set containing the sections in this ini configuration. Note that
471 * changes to this set do not affect the configuration.
472 *
473 * @return a set containing the sections.
474 */
475 public Set getSections()
476 {
477 Set sections = new TreeSet();
478
479 Iterator keys = getKeys();
480 while (keys.hasNext())
481 {
482 String key = (String) keys.next();
483 int index = key.indexOf(".");
484 if (index >= 0)
485 {
486 sections.add(key.substring(0, index));
487 }
488 }
489
490 return sections;
491 }
492 }