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.ViewNode;
034    import org.apache.commons.lang.StringUtils;
035    
036    /**
037     * <p>
038     * A specialized hierarchical configuration implementation for parsing ini
039     * files.
040     * </p>
041     * <p>
042     * An initialization or ini file is a configuration file typically found on
043     * Microsoft's Windows operating system and contains data for Windows based
044     * applications.
045     * </p>
046     * <p>
047     * Although popularized by Windows, ini files can be used on any system or
048     * platform due to the fact that they are merely text files that can easily be
049     * parsed and modified by both humans and computers.
050     * </p>
051     * <p>
052     * A typcial ini file could look something like:
053     * </p>
054     * <code>
055     * [section1]<br>
056     * ; this is a comment!<br>
057     * var1 = foo<br>
058     * var2 = bar<br>
059     * <br>
060     * [section2]<br>
061     * var1 = doo<br>
062     * </code>
063     * <p>
064     * The format of ini files is fairly straight forward and is composed of three
065     * components:<br>
066     * <ul>
067     * <li><b>Sections:</b> Ini files are split into sections, each section starting
068     * with a section declaration. A section declaration starts with a '[' and ends
069     * with a ']'. Sections occur on one line only.</li>
070     * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters
071     * have a typical <code>key = value</code> format.</li>
072     * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
073     * </ul>
074     * </p>
075     * <p>
076     * There are various implementations of the ini file format by various vendors
077     * which has caused a number of differences to appear. As far as possible this
078     * configuration tries to be lenient and support most of the differences.
079     * </p>
080     * <p>
081     * Some of the differences supported are as follows:
082     * <ul>
083     * <li><b>Comments:</b> The '#' character is also accepted as a comment
084     * signifier.</li>
085     * <li><b>Key value separtor:</b> The ':' character is also accepted in place of
086     * '=' to separate keys and values in parameters, for example
087     * <code>var1 : foo</code>.</li>
088     * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed,
089     * this configuration does however support this feature. In the event of a duplicate
090     * section, the two section's values are merged so that there is only a single
091     * section. <strong>Note</strong>: This also affects the internal data of the
092     * configuration. If it is saved, only a single section is written!</li>
093     * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
094     * allowed if they are in two different sections, thus they are local to
095     * sections; this configuration simply merges duplicates; if a section has a
096     * duplicate parameter the values are then added to the key as a list.</li>
097     * </ul>
098     * </p>
099     * <p>
100     * Global parameters are also allowed; any parameters declared before a section
101     * is declared are added to a global section. It is important to note that this
102     * global section does not have a name.
103     * </p>
104     * <p>
105     * In all instances, a parameter's key is prepended with its section name and a
106     * '.' (period). Thus a parameter named "var1" in "section1" will have the key
107     * <code>section1.var1</code> in this configuration. (This is the default
108     * behavior. Because this is a hierarchical configuration you can change this by
109     * setting a different {@link org.apache.commons.configuration.tree.ExpressionEngine}.)
110     * </p>
111     * <p>
112     * <h3>Implementation Details:</h3> Consider the following ini file:<br>
113     * <code>
114     *  default = ok<br>
115     *  <br>
116     *  [section1]<br>
117     *  var1 = foo<br>
118     *  var2 = doodle<br>
119     *   <br>
120     *  [section2]<br>
121     *  ; a comment<br>
122     *  var1 = baz<br>
123     *  var2 = shoodle<br>
124     *  bad =<br>
125     *  = worse<br>
126     *  <br>
127     *  [section3]<br>
128     *  # another comment<br>
129     *  var1 : foo<br>
130     *  var2 : bar<br>
131     *  var5 : test1<br>
132     *  <br>
133     *  [section3]<br>
134     *  var3 = foo<br>
135     *  var4 = bar<br>
136     *  var5 = test2<br>
137     *  <br>
138     *  [sectionSeparators]<br>
139     *  passwd : abc=def<br>
140     *  a:b = "value"
141     *  </code>
142     * </p>
143     * <p>
144     * This ini file will be parsed without error. Note:
145     * <ul>
146     * <li>The parameter named "default" is added to the global section, it's value
147     * is accessed simply using <code>getProperty("default")</code>.</li>
148     * <li>Section 1's parameters can be accessed using
149     * <code>getProperty("section1.var1")</code>.</li>
150     * <li>The parameter named "bad" simply adds the parameter with an empty value.</li>
151     * <li>The empty key with value "= worse" is added using a key consisting of a
152     * single space character. This key is still added to section 2 and the value
153     * can be accessed using <code>getProperty("section2. ")</code>, notice the
154     * period '.' and the space following the section name.</li>
155     * <li>Section three uses both '=' and ':' to separate keys and values.</li>
156     * <li>Section 3 has a duplicate key named "var5". The value for this key is
157     * [test1, test2], and is represented as a List.</li>
158     * <li>The section called <em>sectionSeparators</em> demonstrates how the
159     * configuration deals with multiple occurrences of separator characters. Per
160     * default the first separator character in a line is detected and used to
161     * split the key from the value. Therefore the first property definition in this
162     * section has the key <code>passwd</code> and the value <code>abc=def</code>.
163     * This default behavior can be changed by using quotes. If there is a separator
164     * character before the first quote character (ignoring whitespace), this
165     * character is used as separator. Thus the second property definition in the
166     * section has the key <code>a:b</code> and the value <code>value</code>.</li>
167     * </ul>
168     * </p>
169     * <p>
170     * Internally, this configuration maps the content of the represented ini file
171     * to its node structure in the following way:
172     * <ul>
173     * <li>Sections are represented by direct child nodes of the root node.</li>
174     * <li>For the content of a section, corresponding nodes are created as children
175     * of the section node.</li>
176     * </ul>
177     * This explains how the keys for the properties can be constructed. You can
178     * also use other methods of {@link HierarchicalConfiguration} for querying or
179     * manipulating the hierarchy of configuration nodes, for instance the
180     * <code>configurationAt()</code> method for obtaining the data of a specific
181     * section. However, be careful that the storage scheme described above is not
182     * violated (e.g. by adding multiple levels of nodes or inserting duplicate
183     * section nodes). Otherwise, the special methods for ini configurations may not
184     * work correctly!
185     * </p>
186     * <p>
187     * The set of sections in this configuration can be retrieved using the
188     * <code>getSections()</code> method. For obtaining a
189     * <code>SubnodeConfiguration</code> with the content of a specific section the
190     * <code>getSection()</code> method can be used.
191     * </p>
192     * <p>
193     * <em>Note:</em> Configuration objects of this type can be read concurrently by
194     * multiple threads. However if one of these threads modifies the object,
195     * synchronization has to be performed manually.
196     * </p>
197     *
198     * @author <a
199     *         href="http://commons.apache.org/configuration/team-list.html">Commons
200     *         Configuration team</a>
201     * @version $Id: HierarchicalINIConfiguration.java 1158123 2011-08-16 06:22:16Z oheger $
202     * @since 1.6
203     */
204    public class HierarchicalINIConfiguration extends
205            AbstractHierarchicalFileConfiguration
206    {
207        /**
208         * The characters that signal the start of a comment line.
209         */
210        protected static final String COMMENT_CHARS = "#;";
211    
212        /**
213         * The characters used to separate keys from values.
214         */
215        protected static final String SEPARATOR_CHARS = "=:";
216    
217        /**
218         * The serial version UID.
219         */
220        private static final long serialVersionUID = 2548006161386850670L;
221    
222        /**
223         * Constant for the line separator.
224         */
225        private static final String LINE_SEPARATOR = System.getProperty("line.separator");
226    
227        /**
228         * The characters used for quoting values.
229         */
230        private static final String QUOTE_CHARACTERS = "\"'";
231    
232        /**
233         * The line continuation character.
234         */
235        private static final String LINE_CONT = "\\";
236    
237        /**
238         * Create a new empty INI Configuration.
239         */
240        public HierarchicalINIConfiguration()
241        {
242            super();
243        }
244    
245        /**
246         * Create and load the ini configuration from the given file.
247         *
248         * @param filename The name pr path of the ini file to load.
249         * @throws ConfigurationException If an error occurs while loading the file
250         */
251        public HierarchicalINIConfiguration(String filename)
252                throws ConfigurationException
253        {
254            super(filename);
255        }
256    
257        /**
258         * Create and load the ini configuration from the given file.
259         *
260         * @param file The ini file to load.
261         * @throws ConfigurationException If an error occurs while loading the file
262         */
263        public HierarchicalINIConfiguration(File file)
264                throws ConfigurationException
265        {
266            super(file);
267        }
268    
269        /**
270         * Create and load the ini configuration from the given url.
271         *
272         * @param url The url of the ini file to load.
273         * @throws ConfigurationException If an error occurs while loading the file
274         */
275        public HierarchicalINIConfiguration(URL url) throws ConfigurationException
276        {
277            super(url);
278        }
279    
280        /**
281         * Save the configuration to the specified writer.
282         *
283         * @param writer - The writer to save the configuration to.
284         * @throws ConfigurationException If an error occurs while writing the
285         *         configuration
286         */
287        public void save(Writer writer) throws ConfigurationException
288        {
289            PrintWriter out = new PrintWriter(writer);
290            Iterator it = getSections().iterator();
291            while (it.hasNext())
292            {
293                String section = (String) it.next();
294                Configuration subset;
295                if (section != null)
296                {
297                    out.print("[");
298                    out.print(section);
299                    out.print("]");
300                    out.println();
301                    subset = createSubnodeConfiguration(getSectionNode(section));
302                }
303                else
304                {
305                    subset = getSection(null);
306                }
307    
308                Iterator keys = subset.getKeys();
309                while (keys.hasNext())
310                {
311                    String key = (String) keys.next();
312                    Object value = subset.getProperty(key);
313                    if (value instanceof Collection)
314                    {
315                        Iterator values = ((Collection) value).iterator();
316                        while (values.hasNext())
317                        {
318                            value = (Object) values.next();
319                            out.print(key);
320                            out.print(" = ");
321                            out.print(formatValue(value.toString()));
322                            out.println();
323                        }
324                    }
325                    else
326                    {
327                        out.print(key);
328                        out.print(" = ");
329                        out.print(formatValue(value.toString()));
330                        out.println();
331                    }
332                }
333    
334                out.println();
335            }
336    
337            out.flush();
338        }
339    
340        /**
341         * Load the configuration from the given reader. Note that the
342         * <code>clear</code> method is not called so the configuration read in will
343         * be merged with the current configuration.
344         *
345         * @param reader The reader to read the configuration from.
346         * @throws ConfigurationException If an error occurs while reading the
347         *         configuration
348         */
349        public void load(Reader reader) throws ConfigurationException
350        {
351            try
352            {
353                BufferedReader bufferedReader = new BufferedReader(reader);
354                ConfigurationNode sectionNode = getRootNode();
355    
356                String line = bufferedReader.readLine();
357                while (line != null)
358                {
359                    line = line.trim();
360                    if (!isCommentLine(line))
361                    {
362                        if (isSectionLine(line))
363                        {
364                            String section = line.substring(1, line.length() - 1);
365                            sectionNode = getSectionNode(section);
366                        }
367    
368                        else
369                        {
370                            String key = "";
371                            String value = "";
372                            int index = findSeparator(line);
373                            if (index >= 0)
374                            {
375                                key = line.substring(0, index);
376                                value = parseValue(line.substring(index + 1), bufferedReader);
377                            }
378                            else
379                            {
380                                key = line;
381                            }
382                            key = key.trim();
383                            if (key.length() < 1)
384                            {
385                                // use space for properties with no key
386                                key = " ";
387                            }
388                            ConfigurationNode node = createNode(key);
389                            node.setValue(value);
390                            sectionNode.addChild(node);
391                        }
392                    }
393    
394                    line = bufferedReader.readLine();
395                }
396            }
397            catch (IOException e)
398            {
399                throw new ConfigurationException(
400                        "Unable to load the configuration", e);
401            }
402        }
403    
404        /**
405         * Parse the value to remove the quotes and ignoring the comment. Example:
406         *
407         * <pre>
408         * &quot;value&quot; ; comment -&gt; value
409         * </pre>
410         *
411         * <pre>
412         * 'value' ; comment -&gt; value
413         * </pre>
414         * Note that a comment character is only recognized if there is at least one
415         * whitespace character before it. So it can appear in the property value,
416         * e.g.:
417         * <pre>
418         * C:\\Windows;C:\\Windows\\system32
419         * </pre>
420         *
421         * @param val the value to be parsed
422         * @param reader the reader (needed if multiple lines have to be read)
423         * @throws IOException if an IO error occurs
424         */
425        private static String parseValue(String val, BufferedReader reader) throws IOException
426        {
427            StringBuffer propertyValue = new StringBuffer();
428            boolean lineContinues;
429            String value = val.trim();
430    
431            do
432            {
433                boolean quoted = value.startsWith("\"") || value.startsWith("'");
434                boolean stop = false;
435                boolean escape = false;
436    
437                char quote = quoted ? value.charAt(0) : 0;
438    
439                int i = quoted ? 1 : 0;
440    
441                StringBuffer result = new StringBuffer();
442                char lastChar = 0;
443                while (i < value.length() && !stop)
444                {
445                    char c = value.charAt(i);
446    
447                    if (quoted)
448                    {
449                        if ('\\' == c && !escape)
450                        {
451                            escape = true;
452                        }
453                        else if (!escape && quote == c)
454                        {
455                            stop = true;
456                        }
457                        else if (escape && quote == c)
458                        {
459                            escape = false;
460                            result.append(c);
461                        }
462                        else
463                        {
464                            if (escape)
465                            {
466                                escape = false;
467                                result.append('\\');
468                            }
469    
470                            result.append(c);
471                        }
472                    }
473                    else
474                    {
475                        if (isCommentChar(c) && Character.isWhitespace(lastChar))
476                        {
477                            stop = true;
478                        }
479                        else
480                        {
481                            result.append(c);
482                        }
483                    }
484    
485                    i++;
486                    lastChar = c;
487                }
488    
489                String v = result.toString();
490                if (!quoted)
491                {
492                    v = v.trim();
493                    lineContinues = lineContinues(v);
494                    if (lineContinues)
495                    {
496                        // remove trailing "\"
497                        v = v.substring(0, v.length() - 1).trim();
498                    }
499                }
500                else
501                {
502                    lineContinues = lineContinues(value, i);
503                }
504                propertyValue.append(v);
505    
506                if (lineContinues)
507                {
508                    propertyValue.append(LINE_SEPARATOR);
509                    value = reader.readLine();
510                }
511            } while (lineContinues && value != null);
512    
513            return propertyValue.toString();
514        }
515    
516        /**
517         * Tests whether the specified string contains a line continuation marker.
518         *
519         * @param line the string to check
520         * @return a flag whether this line continues
521         */
522        private static boolean lineContinues(String line)
523        {
524            String s = line.trim();
525            return s.equals(LINE_CONT)
526                    || (s.length() > 2 && s.endsWith(LINE_CONT) && Character
527                            .isWhitespace(s.charAt(s.length() - 2)));
528        }
529    
530        /**
531         * Tests whether the specified string contains a line continuation marker
532         * after the specified position. This method parses the string to remove a
533         * comment that might be present. Then it checks whether a line continuation
534         * marker can be found at the end.
535         *
536         * @param line the line to check
537         * @param pos the start position
538         * @return a flag whether this line continues
539         */
540        private static boolean lineContinues(String line, int pos)
541        {
542            String s;
543    
544            if (pos >= line.length())
545            {
546                s = line;
547            }
548            else
549            {
550                int end = pos;
551                while (end < line.length() && !isCommentChar(line.charAt(end)))
552                {
553                    end++;
554                }
555                s = line.substring(pos, end);
556            }
557    
558            return lineContinues(s);
559        }
560    
561        /**
562         * Tests whether the specified character is a comment character.
563         *
564         * @param c the character
565         * @return a flag whether this character starts a comment
566         */
567        private static boolean isCommentChar(char c)
568        {
569            return COMMENT_CHARS.indexOf(c) >= 0;
570        }
571    
572        /**
573         * Tries to find the index of the separator character in the given string.
574         * This method checks for the presence of separator characters in the given
575         * string. If multiple characters are found, the first one is assumed to be
576         * the correct separator. If there are quoting characters, they are taken
577         * into account, too.
578         *
579         * @param line the line to be checked
580         * @return the index of the separator character or -1 if none is found
581         */
582        private static int findSeparator(String line)
583        {
584            int index =
585                    findSeparatorBeforeQuote(line,
586                            findFirstOccurrence(line, QUOTE_CHARACTERS));
587            if (index < 0)
588            {
589                index = findFirstOccurrence(line, SEPARATOR_CHARS);
590            }
591            return index;
592        }
593    
594        /**
595         * Checks for the occurrence of the specified separators in the given line.
596         * The index of the first separator is returned.
597         *
598         * @param line the line to be investigated
599         * @param separators a string with the separator characters to look for
600         * @return the lowest index of a separator character or -1 if no separator
601         *         is found
602         */
603        private static int findFirstOccurrence(String line, String separators)
604        {
605            int index = -1;
606    
607            for (int i = 0; i < separators.length(); i++)
608            {
609                char sep = separators.charAt(i);
610                int pos = line.indexOf(sep);
611                if (pos >= 0)
612                {
613                    if (index < 0 || pos < index)
614                    {
615                        index = pos;
616                    }
617                }
618            }
619    
620            return index;
621        }
622    
623        /**
624         * Searches for a separator character directly before a quoting character.
625         * If the first non-whitespace character before a quote character is a
626         * separator, it is considered the "real" separator in this line - even if
627         * there are other separators before.
628         *
629         * @param line the line to be investigated
630         * @param quoteIndex the index of the quote character
631         * @return the index of the separator before the quote or &lt; 0 if there is
632         *         none
633         */
634        private static int findSeparatorBeforeQuote(String line, int quoteIndex)
635        {
636            int index = quoteIndex - 1;
637            while (index >= 0 && Character.isWhitespace(line.charAt(index)))
638            {
639                index--;
640            }
641    
642            if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0)
643            {
644                index = -1;
645            }
646    
647            return index;
648        }
649    
650        /**
651         * Add quotes around the specified value if it contains a comment character.
652         */
653        private String formatValue(String value)
654        {
655            boolean quoted = false;
656    
657            for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
658            {
659                char c = COMMENT_CHARS.charAt(i);
660                if (value.indexOf(c) != -1)
661                {
662                    quoted = true;
663                }
664            }
665    
666            if (quoted)
667            {
668                return '"' + StringUtils.replace(value, "\"", "\\\"") + '"';
669            }
670            else
671            {
672                return value;
673            }
674        }
675    
676        /**
677         * Determine if the given line is a comment line.
678         *
679         * @param line The line to check.
680         * @return true if the line is empty or starts with one of the comment
681         *         characters
682         */
683        protected boolean isCommentLine(String line)
684        {
685            if (line == null)
686            {
687                return false;
688            }
689            // blank lines are also treated as comment lines
690            return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
691        }
692    
693        /**
694         * Determine if the given line is a section.
695         *
696         * @param line The line to check.
697         * @return true if the line contains a secion
698         */
699        protected boolean isSectionLine(String line)
700        {
701            if (line == null)
702            {
703                return false;
704            }
705            return line.startsWith("[") && line.endsWith("]");
706        }
707    
708        /**
709         * Return a set containing the sections in this ini configuration. Note that
710         * changes to this set do not affect the configuration.
711         *
712         * @return a set containing the sections.
713         */
714        public Set getSections()
715        {
716            Set sections = new ListOrderedSet();
717            boolean globalSection = false;
718            boolean inSection = false;
719    
720            for (Iterator it = getRootNode().getChildren().iterator(); it.hasNext();)
721            {
722                ConfigurationNode node = (ConfigurationNode) it.next();
723                if (isSectionNode(node))
724                {
725                    inSection = true;
726                    sections.add(node.getName());
727                }
728                else
729                {
730                    if (!inSection && !globalSection)
731                    {
732                        globalSection = true;
733                        sections.add(null);
734                    }
735                }
736            }
737    
738            return sections;
739        }
740    
741        /**
742         * Returns a configuration with the content of the specified section. This
743         * provides an easy way of working with a single section only. The way this
744         * configuration is structured internally, this method is very similar to
745         * calling {@link HierarchicalConfiguration#configurationAt(String)} with
746         * the name of the section in question. There are the following differences
747         * however:
748         * <ul>
749         * <li>This method never throws an exception. If the section does not exist,
750         * it is created now. The configuration returned in this case is empty.</li>
751         * <li>If section is contained multiple times in the configuration, the
752         * configuration returned by this method is initialized with the first
753         * occurrence of the section. (This can only happen if
754         * <code>addProperty()</code> has been used in a way that does not conform
755         * to the storage scheme used by <code>HierarchicalINIConfiguration</code>.
756         * If used correctly, there will not be duplicate sections.)</li>
757         * <li>There is special support for the global section: Passing in
758         * <b>null</b> as section name returns a configuration with the content of
759         * the global section (which may also be empty).</li>
760         * </ul>
761         *
762         * @param name the name of the section in question; <b>null</b> represents
763         *        the global section
764         * @return a configuration containing only the properties of the specified
765         *         section
766         */
767        public SubnodeConfiguration getSection(String name)
768        {
769            if (name == null)
770            {
771                return getGlobalSection();
772            }
773    
774            else
775            {
776                try
777                {
778                    return configurationAt(name);
779                }
780                catch (IllegalArgumentException iex)
781                {
782                    // the passed in key does not map to exactly one node
783                    // obtain the node for the section, create it on demand
784                    return new SubnodeConfiguration(this, getSectionNode(name));
785                }
786            }
787        }
788    
789        /**
790         * Obtains the node representing the specified section. This method is
791         * called while the configuration is loaded. If a node for this section
792         * already exists, it is returned. Otherwise a new node is created.
793         *
794         * @param sectionName the name of the section
795         * @return the node for this section
796         */
797        private ConfigurationNode getSectionNode(String sectionName)
798        {
799            List nodes = getRootNode().getChildren(sectionName);
800            if (!nodes.isEmpty())
801            {
802                return (ConfigurationNode) nodes.get(0);
803            }
804    
805            ConfigurationNode node = createNode(sectionName);
806            markSectionNode(node);
807            getRootNode().addChild(node);
808            return node;
809        }
810    
811        /**
812         * Creates a sub configuration for the global section of the represented INI
813         * configuration.
814         *
815         * @return the sub configuration for the global section
816         */
817        private SubnodeConfiguration getGlobalSection()
818        {
819            ViewNode parent = new ViewNode();
820    
821            for (Iterator it = getRootNode().getChildren().iterator(); it.hasNext();)
822            {
823                ConfigurationNode node = (ConfigurationNode) it.next();
824                if (!isSectionNode(node))
825                {
826                    synchronized (node)
827                    {
828                        parent.addChild(node);
829                    }
830                }
831            }
832    
833            return createSubnodeConfiguration(parent);
834        }
835    
836        /**
837         * Marks a configuration node as a section node. This means that this node
838         * represents a section header. This implementation uses the node's
839         * reference property to store a flag.
840         *
841         * @param node the node to be marked
842         */
843        private static void markSectionNode(ConfigurationNode node)
844        {
845            node.setReference(Boolean.TRUE);
846        }
847    
848        /**
849         * Checks whether the specified configuration node represents a section.
850         *
851         * @param node the node in question
852         * @return a flag whether this node represents a section
853         */
854        private static boolean isSectionNode(ConfigurationNode node)
855        {
856            return node.getReference() != null || node.getChildrenCount() > 0;
857        }
858    }