001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.configuration;
018    
019    import java.io.BufferedReader;
020    import java.io.File;
021    import java.io.IOException;
022    import java.io.PrintWriter;
023    import java.io.Reader;
024    import java.io.Writer;
025    import java.net.URL;
026    import java.util.Collection;
027    import java.util.Iterator;
028    import java.util.List;
029    import java.util.Set;
030    
031    import org.apache.commons.collections.set.ListOrderedSet;
032    import org.apache.commons.configuration.tree.ConfigurationNode;
033    import org.apache.commons.configuration.tree.DefaultConfigurationNode;
034    import org.apache.commons.configuration.tree.ViewNode;
035    import org.apache.commons.lang.StringUtils;
036    
037    /**
038     * <p>
039     * A specialized hierarchical configuration implementation for parsing ini
040     * files.
041     * </p>
042     * <p>
043     * An initialization or ini file is a configuration file typically found on
044     * Microsoft's Windows operating system and contains data for Windows based
045     * applications.
046     * </p>
047     * <p>
048     * Although popularized by Windows, ini files can be used on any system or
049     * platform due to the fact that they are merely text files that can easily be
050     * parsed and modified by both humans and computers.
051     * </p>
052     * <p>
053     * A typcial ini file could look something like:
054     * </p>
055     * <code>
056     * [section1]<br>
057     * ; this is a comment!<br>
058     * var1 = foo<br>
059     * var2 = bar<br>
060     * <br>
061     * [section2]<br>
062     * var1 = doo<br>
063     * </code>
064     * <p>
065     * The format of ini files is fairly straight forward and is composed of three
066     * components:<br>
067     * <ul>
068     * <li><b>Sections:</b> Ini files are split into sections, each section starting
069     * with a section declaration. A section declaration starts with a '[' and ends
070     * with a ']'. Sections occur on one line only.</li>
071     * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters
072     * have a typical <code>key = value</code> format.</li>
073     * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
074     * </ul>
075     * </p>
076     * <p>
077     * There are various implementations of the ini file format by various vendors
078     * which has caused a number of differences to appear. As far as possible this
079     * configuration tries to be lenient and support most of the differences.
080     * </p>
081     * <p>
082     * Some of the differences supported are as follows:
083     * <ul>
084     * <li><b>Comments:</b> The '#' character is also accepted as a comment
085     * signifier.</li>
086     * <li><b>Key value separtor:</b> The ':' character is also accepted in place of
087     * '=' to separate keys and values in parameters, for example
088     * <code>var1 : foo</code>.</li>
089     * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed,
090     * this configuration does however support it. In the event of a duplicate
091     * section, the two section's values are merged.</li>
092     * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
093     * allowed if they are in two different sections, thus they are local to
094     * sections; this configuration simply merges duplicates; if a section has a
095     * duplicate parameter the values are then added to the key as a list.</li>
096     * </ul>
097     * </p>
098     * <p>
099     * 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    }