1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.configuration;
18
19 import java.io.BufferedReader;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.PrintWriter;
23 import java.io.Reader;
24 import java.io.Writer;
25 import java.net.URL;
26 import java.util.Collection;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Set;
30
31 import org.apache.commons.collections.set.ListOrderedSet;
32 import org.apache.commons.configuration.tree.ConfigurationNode;
33 import org.apache.commons.configuration.tree.DefaultConfigurationNode;
34 import org.apache.commons.configuration.tree.ViewNode;
35 import org.apache.commons.lang.StringUtils;
36
37 /***
38 * <p>
39 * A specialized hierarchical configuration implementation for parsing ini
40 * files.
41 * </p>
42 * <p>
43 * An initialization or ini file is a configuration file typically found on
44 * Microsoft's Windows operating system and contains data for Windows based
45 * applications.
46 * </p>
47 * <p>
48 * Although popularized by Windows, ini files can be used on any system or
49 * platform due to the fact that they are merely text files that can easily be
50 * parsed and modified by both humans and computers.
51 * </p>
52 * <p>
53 * A typcial ini file could look something like:
54 * </p>
55 * <code>
56 * [section1]<br>
57 * ; this is a comment!<br>
58 * var1 = foo<br>
59 * var2 = bar<br>
60 * <br>
61 * [section2]<br>
62 * var1 = doo<br>
63 * </code>
64 * <p>
65 * The format of ini files is fairly straight forward and is composed of three
66 * components:<br>
67 * <ul>
68 * <li><b>Sections:</b> Ini files are split into sections, each section starting
69 * with a section declaration. A section declaration starts with a '[' and ends
70 * with a ']'. Sections occur on one line only.</li>
71 * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters
72 * have a typical <code>key = value</code> format.</li>
73 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
74 * </ul>
75 * </p>
76 * <p>
77 * There are various implementations of the ini file format by various vendors
78 * which has caused a number of differences to appear. As far as possible this
79 * configuration tries to be lenient and support most of the differences.
80 * </p>
81 * <p>
82 * Some of the differences supported are as follows:
83 * <ul>
84 * <li><b>Comments:</b> The '#' character is also accepted as a comment
85 * signifier.</li>
86 * <li><b>Key value separtor:</b> The ':' character is also accepted in place of
87 * '=' to separate keys and values in parameters, for example
88 * <code>var1 : foo</code>.</li>
89 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed,
90 * this configuration does however support it. In the event of a duplicate
91 * section, the two section's values are merged.</li>
92 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
93 * allowed if they are in two different sections, thus they are local to
94 * sections; this configuration simply merges duplicates; if a section has a
95 * duplicate parameter the values are then added to the key as a list.</li>
96 * </ul>
97 * </p>
98 * <p>
99 * Global parameters are also allowed; any parameters declared before a section
100 * is declared are added to a global section. It is important to note that this
101 * global section does not have a name.
102 * </p>
103 * <p>
104 * In all instances, a parameter's key is prepended with its section name and a
105 * '.' (period). Thus a parameter named "var1" in "section1" will have the key
106 * <code>section1.var1</code> in this configuration. (This is the default
107 * behavior. Because this is a hierarchical configuration you can change this by
108 * setting a different {@link org.apache.commons.configuration.tree.ExpressionEngine}.)
109 * </p>
110 * <p>
111 * <h3>Implementation Details:</h3> 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.</li>
146 * <li>The empty key with value "= worse" is added using a key consisting of a
147 * single space character. This key is still added to section 2 and the value
148 * can be accessed using <code>getProperty("section2. ")</code>, notice the
149 * period '.' and the space following the section name.</li>
150 * <li>Section three uses both '=' and ':' to separate keys and values.</li>
151 * <li>Section 3 has a duplicate key named "var5". The value for this key is
152 * [test1, test2], and is represented as a List.</li>
153 * </ul>
154 * </p>
155 * <p>
156 * Internally, this configuration maps the content of the represented ini file
157 * to its node structure in the following way:
158 * <ul>
159 * <li>Sections are represented by direct child nodes of the root node.</li>
160 * <li>For the content of a section, corresponding nodes are created as children
161 * of the section node.</li>
162 * </ul>
163 * This explains how the keys for the properties can be constructed. You can
164 * also use other methods of {@link HierarchicalConfiguration} for querying or
165 * manipulating the hierarchy of configuration nodes, for instance the
166 * <code>configurationAt()</code> method for obtaining the data of a specific
167 * section.
168 * </p>
169 * <p>
170 * The set of sections in this configuration can be retrieved using the
171 * <code>getSections()</code> method. For obtaining a
172 * <code>SubnodeConfiguration</code> with the content of a specific section the
173 * <code>getSection()</code> method can be used.
174 * </p>
175 * <p>
176 * <em>Note:</em> Configuration objects of this type can be read concurrently by
177 * multiple threads. However if one of these threads modifies the object,
178 * synchronization has to be performed manually.
179 * </p>
180 *
181 * @author <a
182 * href="http://commons.apache.org/configuration/team-list.html">Commons
183 * Configuration team</a>
184 * @version $Id: HierarchicalINIConfiguration.java 720295 2008-11-24 21:29:42Z oheger $
185 * @since 1.6
186 */
187 public class HierarchicalINIConfiguration extends
188 AbstractHierarchicalFileConfiguration
189 {
190 /***
191 * The characters that signal the start of a comment line.
192 */
193 protected static final String COMMENT_CHARS = "#;";
194
195 /***
196 * The characters used to separate keys from values.
197 */
198 protected static final String SEPARATOR_CHARS = "=:";
199
200 /***
201 * The serial version UID.
202 */
203 private static final long serialVersionUID = 2548006161386850670L;
204
205 /***
206 * Constant for the line separator.
207 */
208 private static final String LINE_SEPARATOR = System.getProperty("line.separator");
209
210 /***
211 * The line continuation character.
212 */
213 private static final String LINE_CONT = "//";
214
215 /***
216 * Create a new empty INI Configuration.
217 */
218 public HierarchicalINIConfiguration()
219 {
220 super();
221 }
222
223 /***
224 * Create and load the ini configuration from the given file.
225 *
226 * @param filename The name pr path of the ini file to load.
227 * @throws ConfigurationException If an error occurs while loading the file
228 */
229 public HierarchicalINIConfiguration(String filename)
230 throws ConfigurationException
231 {
232 super(filename);
233 }
234
235 /***
236 * Create and load the ini configuration from the given file.
237 *
238 * @param file The ini file to load.
239 * @throws ConfigurationException If an error occurs while loading the file
240 */
241 public HierarchicalINIConfiguration(File file)
242 throws ConfigurationException
243 {
244 super(file);
245 }
246
247 /***
248 * Create and load the ini configuration from the given url.
249 *
250 * @param url The url of the ini file to load.
251 * @throws ConfigurationException If an error occurs while loading the file
252 */
253 public HierarchicalINIConfiguration(URL url) throws ConfigurationException
254 {
255 super(url);
256 }
257
258 /***
259 * Save the configuration to the specified writer.
260 *
261 * @param writer - The writer to save the configuration to.
262 * @throws ConfigurationException If an error occurs while writing the
263 * configuration
264 */
265 public void save(Writer writer) throws ConfigurationException
266 {
267 PrintWriter out = new PrintWriter(writer);
268 Iterator it = getSections().iterator();
269 while (it.hasNext())
270 {
271 String section = (String) it.next();
272 if (section != null)
273 {
274 out.print("[");
275 out.print(section);
276 out.print("]");
277 out.println();
278 }
279
280 Configuration subset = getSection(section);
281 Iterator keys = subset.getKeys();
282 while (keys.hasNext())
283 {
284 String key = (String) keys.next();
285 Object value = subset.getProperty(key);
286 if (value instanceof Collection)
287 {
288 Iterator values = ((Collection) value).iterator();
289 while (values.hasNext())
290 {
291 value = (Object) values.next();
292 out.print(key);
293 out.print(" = ");
294 out.print(formatValue(value.toString()));
295 out.println();
296 }
297 }
298 else
299 {
300 out.print(key);
301 out.print(" = ");
302 out.print(formatValue(value.toString()));
303 out.println();
304 }
305 }
306
307 out.println();
308 }
309
310 out.flush();
311 }
312
313 /***
314 * Load the configuration from the given reader. Note that the
315 * <code>clear</code> method is not called so the configuration read in will
316 * be merged with the current configuration.
317 *
318 * @param reader The reader to read the configuration from.
319 * @throws ConfigurationException If an error occurs while reading the
320 * configuration
321 */
322 public void load(Reader reader) throws ConfigurationException
323 {
324 try
325 {
326 BufferedReader bufferedReader = new BufferedReader(reader);
327 ConfigurationNode sectionNode = getRootNode();
328
329 String line = bufferedReader.readLine();
330 while (line != null)
331 {
332 line = line.trim();
333 if (!isCommentLine(line))
334 {
335 if (isSectionLine(line))
336 {
337 String section = line.substring(1, line.length() - 1);
338 sectionNode = getSectionNode(section);
339 }
340
341 else
342 {
343 String key = "";
344 String value = "";
345 int index = line.indexOf("=");
346 if (index >= 0)
347 {
348 key = line.substring(0, index);
349 value = parseValue(line.substring(index + 1), bufferedReader);
350 }
351 else
352 {
353 index = line.indexOf(":");
354 if (index >= 0)
355 {
356 key = line.substring(0, index);
357 value = parseValue(line.substring(index + 1), bufferedReader);
358 }
359 else
360 {
361 key = line;
362 }
363 }
364 key = key.trim();
365 if (key.length() < 1)
366 {
367
368 key = " ";
369 }
370 ConfigurationNode node = createNode(key);
371 node.setValue(value);
372 sectionNode.addChild(node);
373 }
374 }
375
376 line = bufferedReader.readLine();
377 }
378 }
379 catch (IOException e)
380 {
381 throw new ConfigurationException(
382 "Unable to load the configuration", e);
383 }
384 }
385
386 /***
387 * Parse the value to remove the quotes and ignoring the comment. Example:
388 *
389 * <pre>
390 * "value" ; comment -> value
391 * </pre>
392 *
393 * <pre>
394 * 'value' ; comment -> value
395 * </pre>
396 *
397 * @param val the value to be parsed
398 * @param reader the reader (needed if multiple lines have to be read)
399 * @throws IOException if an IO error occurs
400 */
401 private static String parseValue(String val, BufferedReader reader) throws IOException
402 {
403 StringBuffer propertyValue = new StringBuffer();
404 boolean lineContinues;
405 String value = val.trim();
406
407 do
408 {
409 boolean quoted = value.startsWith("\"") || value.startsWith("'");
410 boolean stop = false;
411 boolean escape = false;
412
413 char quote = quoted ? value.charAt(0) : 0;
414
415 int i = quoted ? 1 : 0;
416
417 StringBuffer result = new StringBuffer();
418 while (i < value.length() && !stop)
419 {
420 char c = value.charAt(i);
421
422 if (quoted)
423 {
424 if ('//' == c && !escape)
425 {
426 escape = true;
427 }
428 else if (!escape && quote == c)
429 {
430 stop = true;
431 }
432 else if (escape && quote == c)
433 {
434 escape = false;
435 result.append(c);
436 }
437 else
438 {
439 if (escape)
440 {
441 escape = false;
442 result.append('//');
443 }
444
445 result.append(c);
446 }
447 }
448 else
449 {
450 if (!isCommentChar(c))
451 {
452 result.append(c);
453 }
454 else
455 {
456 stop = true;
457 }
458 }
459
460 i++;
461 }
462
463 String v = result.toString();
464 if (!quoted)
465 {
466 v = v.trim();
467 lineContinues = lineContinues(v);
468 if (lineContinues)
469 {
470
471 v = v.substring(0, v.length() - 1).trim();
472 }
473 }
474 else
475 {
476 lineContinues = lineContinues(value, i);
477 }
478 propertyValue.append(v);
479
480 if (lineContinues)
481 {
482 propertyValue.append(LINE_SEPARATOR);
483 value = reader.readLine();
484 }
485 } while (lineContinues && value != null);
486
487 return propertyValue.toString();
488 }
489
490 /***
491 * Tests whether the specified string contains a line continuation marker.
492 *
493 * @param line the string to check
494 * @return a flag whether this line continues
495 */
496 private static boolean lineContinues(String line)
497 {
498 String s = line.trim();
499 return s.equals(LINE_CONT)
500 || (s.length() > 2 && s.endsWith(LINE_CONT) && Character
501 .isWhitespace(s.charAt(s.length() - 2)));
502 }
503
504 /***
505 * Tests whether the specified string contains a line continuation marker
506 * after the specified position. This method parses the string to remove a
507 * comment that might be present. Then it checks whether a line continuation
508 * marker can be found at the end.
509 *
510 * @param line the line to check
511 * @param pos the start position
512 * @return a flag whether this line continues
513 */
514 private static boolean lineContinues(String line, int pos)
515 {
516 String s;
517
518 if (pos >= line.length())
519 {
520 s = line;
521 }
522 else
523 {
524 int end = pos;
525 while (end < line.length() && !isCommentChar(line.charAt(end)))
526 {
527 end++;
528 }
529 s = line.substring(pos, end);
530 }
531
532 return lineContinues(s);
533 }
534
535 /***
536 * Tests whether the specified character is a comment character.
537 *
538 * @param c the character
539 * @return a flag whether this character starts a comment
540 */
541 private static boolean isCommentChar(char c)
542 {
543 return COMMENT_CHARS.indexOf(c) >= 0;
544 }
545
546 /***
547 * Add quotes around the specified value if it contains a comment character.
548 */
549 private String formatValue(String value)
550 {
551 boolean quoted = false;
552
553 for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
554 {
555 char c = COMMENT_CHARS.charAt(i);
556 if (value.indexOf(c) != -1)
557 {
558 quoted = true;
559 }
560 }
561
562 if (quoted)
563 {
564 return '"' + StringUtils.replace(value, "\"", "//\"") + '"';
565 }
566 else
567 {
568 return value;
569 }
570 }
571
572 /***
573 * Determine if the given line is a comment line.
574 *
575 * @param line The line to check.
576 * @return true if the line is empty or starts with one of the comment
577 * characters
578 */
579 protected boolean isCommentLine(String line)
580 {
581 if (line == null)
582 {
583 return false;
584 }
585
586 return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
587 }
588
589 /***
590 * Determine if the given line is a section.
591 *
592 * @param line The line to check.
593 * @return true if the line contains a secion
594 */
595 protected boolean isSectionLine(String line)
596 {
597 if (line == null)
598 {
599 return false;
600 }
601 return line.startsWith("[") && line.endsWith("]");
602 }
603
604 /***
605 * Return a set containing the sections in this ini configuration. Note that
606 * changes to this set do not affect the configuration.
607 *
608 * @return a set containing the sections.
609 */
610 public Set getSections()
611 {
612 Set sections = new ListOrderedSet();
613 boolean globalSection = false;
614
615 for (Iterator it = getRootNode().getChildren().iterator(); it.hasNext();)
616 {
617 ConfigurationNode node = (ConfigurationNode) it.next();
618 if (isSectionNode(node))
619 {
620 if (globalSection)
621 {
622 sections.add(null);
623 globalSection = false;
624 }
625 sections.add(node.getName());
626 }
627 else
628 {
629 globalSection = true;
630 }
631 }
632
633 return sections;
634 }
635
636 /***
637 * Returns a configuration with the content of the specified section. This
638 * provides an easy way of working with a single section only. The way this
639 * configuration is structured internally, this method is very similar to
640 * calling
641 * <code>{@link HierarchicalConfiguration#configurationAt(String)}</code>
642 * with the name of the section in question. There are the following
643 * differences however:
644 * <ul>
645 * <li>This method never throws an exception. If the section does not exist,
646 * an empty configuration is returned.</li>
647 * <li>There is special support for the global section: Passing in
648 * <b>null</b> as section name returns a configuration with the content of
649 * the global section (which may also be empty).</li>
650 * </ul>
651 *
652 * @param name the name of the section in question; <b>null</b> represents
653 * the global section
654 * @return a configuration containing only the properties of the specified
655 * section
656 */
657 public SubnodeConfiguration getSection(String name)
658 {
659 if (name == null)
660 {
661 return getGlobalSection();
662 }
663
664 else
665 {
666 try
667 {
668 return configurationAt(name);
669 }
670 catch (IllegalArgumentException iex)
671 {
672
673
674 return new SubnodeConfiguration(this,
675 new DefaultConfigurationNode());
676 }
677 }
678 }
679
680 /***
681 * Obtains the node representing the specified section. This method is
682 * called while the configuration is loaded. If a node for this section
683 * already exists, it is returned. Otherwise a new node is created.
684 *
685 * @param sectionName the name of the section
686 * @return the node for this section
687 */
688 private ConfigurationNode getSectionNode(String sectionName)
689 {
690 List nodes = getRootNode().getChildren(sectionName);
691 if (!nodes.isEmpty())
692 {
693 return (ConfigurationNode) nodes.get(0);
694 }
695
696 ConfigurationNode node = createNode(sectionName);
697 markSectionNode(node);
698 getRootNode().addChild(node);
699 return node;
700 }
701
702 /***
703 * Creates a sub configuration for the global section of the represented INI
704 * configuration.
705 *
706 * @return the sub configuration for the global section
707 */
708 private SubnodeConfiguration getGlobalSection()
709 {
710 ViewNode parent = new ViewNode();
711
712 for (Iterator it = getRootNode().getChildren().iterator(); it.hasNext();)
713 {
714 ConfigurationNode node = (ConfigurationNode) it.next();
715 if (!isSectionNode(node))
716 {
717 parent.addChild(node);
718 }
719 }
720
721 return createSubnodeConfiguration(parent);
722 }
723
724 /***
725 * Marks a configuration node as a section node. This means that this node
726 * represents a section header. This implementation uses the node's
727 * reference property to store a flag.
728 *
729 * @param node the node to be marked
730 */
731 private static void markSectionNode(ConfigurationNode node)
732 {
733 node.setReference(Boolean.TRUE);
734 }
735
736 /***
737 * Checks whether the specified configuration node represents a section.
738 *
739 * @param node the node in question
740 * @return a flag whether this node represents a section
741 */
742 private static boolean isSectionNode(ConfigurationNode node)
743 {
744 return node.getReference() != null || node.getChildrenCount() > 0;
745 }
746 }