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  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                             // use space for properties with no key
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      * &quot;value&quot; ; comment -&gt; value
391      * </pre>
392      *
393      * <pre>
394      * 'value' ; comment -&gt; 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                     // remove trailing "\"
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         // blank lines are also treated as comment lines
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                 // the passed in key does not map to exactly one node
673                 // return an empty configuration
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 }