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.Collections;
28  import java.util.Iterator;
29  import java.util.LinkedHashSet;
30  import java.util.List;
31  import java.util.Set;
32  
33  import org.apache.commons.configuration.tree.ConfigurationNode;
34  import org.apache.commons.configuration.tree.ViewNode;
35  
36  /**
37   * <p>
38   * A specialized hierarchical configuration implementation for parsing ini
39   * files.
40   * </p>
41   * <p>
42   * An initialization or ini file is a configuration file typically found on
43   * Microsoft's Windows operating system and contains data for Windows based
44   * applications.
45   * </p>
46   * <p>
47   * Although popularized by Windows, ini files can be used on any system or
48   * platform due to the fact that they are merely text files that can easily be
49   * parsed and modified by both humans and computers.
50   * </p>
51   * <p>
52   * A typical ini file could look something like:
53   * </p>
54   * <pre>
55   * [section1]
56   * ; this is a comment!
57   * var1 = foo
58   * var2 = bar
59   *
60   * [section2]
61   * var1 = doo
62   * </pre>
63   * <p>
64   * The format of ini files is fairly straight forward and is composed of three
65   * components:<br>
66   * <ul>
67   * <li><b>Sections:</b> Ini files are split into sections, each section starting
68   * with a section declaration. A section declaration starts with a '[' and ends
69   * with a ']'. Sections occur on one line only.</li>
70   * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters
71   * have a typical {@code key = value} format.</li>
72   * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
73   * </ul>
74   * </p>
75   * <p>
76   * There are various implementations of the ini file format by various vendors
77   * which has caused a number of differences to appear. As far as possible this
78   * configuration tries to be lenient and support most of the differences.
79   * </p>
80   * <p>
81   * Some of the differences supported are as follows:
82   * <ul>
83   * <li><b>Comments:</b> The '#' character is also accepted as a comment
84   * signifier.</li>
85   * <li><b>Key value separator:</b> The ':' character is also accepted in place of
86   * '=' to separate keys and values in parameters, for example
87   * {@code var1 : foo}.</li>
88   * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed,
89   * this configuration does however support this feature. In the event of a duplicate
90   * section, the two section's values are merged so that there is only a single
91   * section. <strong>Note</strong>: This also affects the internal data of the
92   * configuration. If it is saved, only a single section is written!</li>
93   * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
94   * allowed if they are in two different sections, thus they are local to
95   * sections; this configuration simply merges duplicates; if a section has a
96   * duplicate parameter the values are then added to the key as a list.</li>
97   * </ul>
98   * </p>
99   * <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} 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  * <pre>
114  *  default = ok
115  *
116  *  [section1]
117  *  var1 = foo
118  *  var2 = doodle
119  *
120  *  [section2]
121  *  ; a comment
122  *  var1 = baz
123  *  var2 = shoodle
124  *  bad =
125  *  = worse
126  *
127  *  [section3]
128  *  # another comment
129  *  var1 : foo
130  *  var2 : bar
131  *  var5 : test1
132  *
133  *  [section3]
134  *  var3 = foo
135  *  var4 = bar
136  *  var5 = test2
137  *
138  *  [sectionSeparators]
139  *  passwd : abc=def
140  *  a:b = "value"
141  *  </pre>
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")}.</li>
148  * <li>Section 1's parameters can be accessed using
149  * {@code getProperty("section1.var1")}.</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. ")}, 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} and the value {@code abc=def}.
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} and the value {@code value}.</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()} 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()} method. For obtaining a
189  * {@code SubnodeConfiguration} with the content of a specific section the
190  * {@code getSection()} 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 1234362 2012-01-21 16:59:48Z 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<String> it = getSections().iterator();
291         while (it.hasNext())
292         {
293             String section = 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<String> keys = subset.getKeys();
309             while (keys.hasNext())
310             {
311                 String key = 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 = 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()} 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                         createValueNodes(sectionNode, key, value);
389                     }
390                 }
391 
392                 line = bufferedReader.readLine();
393             }
394         }
395         catch (IOException e)
396         {
397             throw new ConfigurationException(
398                     "Unable to load the configuration", e);
399         }
400     }
401 
402     /**
403      * Creates the node(s) for the given key value-pair. If delimiter parsing is
404      * enabled, the value string is split if possible, and for each single value
405      * a node is created. Otherwise only a single node is added to the section.
406      *
407      * @param sectionNode the section node new nodes have to be added
408      * @param key the key
409      * @param value the value string
410      */
411     private void createValueNodes(ConfigurationNode sectionNode, String key,
412             String value)
413     {
414         Collection<String> values;
415         if (isDelimiterParsingDisabled())
416         {
417             values = Collections.singleton(value);
418         }
419         else
420         {
421             values = PropertyConverter.split(value, getListDelimiter(), false);
422         }
423 
424         for (String v : values)
425         {
426             ConfigurationNode node = createNode(key);
427             node.setValue(v);
428             sectionNode.addChild(node);
429         }
430     }
431 
432     /**
433      * Parse the value to remove the quotes and ignoring the comment. Example:
434      *
435      * <pre>
436      * &quot;value&quot; ; comment -&gt; value
437      * </pre>
438      *
439      * <pre>
440      * 'value' ; comment -&gt; value
441      * </pre>
442      * Note that a comment character is only recognized if there is at least one
443      * whitespace character before it. So it can appear in the property value,
444      * e.g.:
445      * <pre>
446      * C:\\Windows;C:\\Windows\\system32
447      * </pre>
448      *
449      * @param val the value to be parsed
450      * @param reader the reader (needed if multiple lines have to be read)
451      * @throws IOException if an IO error occurs
452      */
453     private static String parseValue(String val, BufferedReader reader) throws IOException
454     {
455         StringBuilder propertyValue = new StringBuilder();
456         boolean lineContinues;
457         String value = val.trim();
458 
459         do
460         {
461             boolean quoted = value.startsWith("\"") || value.startsWith("'");
462             boolean stop = false;
463             boolean escape = false;
464 
465             char quote = quoted ? value.charAt(0) : 0;
466 
467             int i = quoted ? 1 : 0;
468 
469             StringBuilder result = new StringBuilder();
470             char lastChar = 0;
471             while (i < value.length() && !stop)
472             {
473                 char c = value.charAt(i);
474 
475                 if (quoted)
476                 {
477                     if ('\\' == c && !escape)
478                     {
479                         escape = true;
480                     }
481                     else if (!escape && quote == c)
482                     {
483                         stop = true;
484                     }
485                     else if (escape && quote == c)
486                     {
487                         escape = false;
488                         result.append(c);
489                     }
490                     else
491                     {
492                         if (escape)
493                         {
494                             escape = false;
495                             result.append('\\');
496                         }
497 
498                         result.append(c);
499                     }
500                 }
501                 else
502                 {
503                     if (isCommentChar(c) && Character.isWhitespace(lastChar))
504                     {
505                         stop = true;
506                     }
507                     else
508                     {
509                         result.append(c);
510                     }
511                 }
512 
513                 i++;
514                 lastChar = c;
515             }
516 
517             String v = result.toString();
518             if (!quoted)
519             {
520                 v = v.trim();
521                 lineContinues = lineContinues(v);
522                 if (lineContinues)
523                 {
524                     // remove trailing "\"
525                     v = v.substring(0, v.length() - 1).trim();
526                 }
527             }
528             else
529             {
530                 lineContinues = lineContinues(value, i);
531             }
532             propertyValue.append(v);
533 
534             if (lineContinues)
535             {
536                 propertyValue.append(LINE_SEPARATOR);
537                 value = reader.readLine();
538             }
539         } while (lineContinues && value != null);
540 
541         return propertyValue.toString();
542     }
543 
544     /**
545      * Tests whether the specified string contains a line continuation marker.
546      *
547      * @param line the string to check
548      * @return a flag whether this line continues
549      */
550     private static boolean lineContinues(String line)
551     {
552         String s = line.trim();
553         return s.equals(LINE_CONT)
554                 || (s.length() > 2 && s.endsWith(LINE_CONT) && Character
555                         .isWhitespace(s.charAt(s.length() - 2)));
556     }
557 
558     /**
559      * Tests whether the specified string contains a line continuation marker
560      * after the specified position. This method parses the string to remove a
561      * comment that might be present. Then it checks whether a line continuation
562      * marker can be found at the end.
563      *
564      * @param line the line to check
565      * @param pos the start position
566      * @return a flag whether this line continues
567      */
568     private static boolean lineContinues(String line, int pos)
569     {
570         String s;
571 
572         if (pos >= line.length())
573         {
574             s = line;
575         }
576         else
577         {
578             int end = pos;
579             while (end < line.length() && !isCommentChar(line.charAt(end)))
580             {
581                 end++;
582             }
583             s = line.substring(pos, end);
584         }
585 
586         return lineContinues(s);
587     }
588 
589     /**
590      * Tests whether the specified character is a comment character.
591      *
592      * @param c the character
593      * @return a flag whether this character starts a comment
594      */
595     private static boolean isCommentChar(char c)
596     {
597         return COMMENT_CHARS.indexOf(c) >= 0;
598     }
599 
600     /**
601      * Tries to find the index of the separator character in the given string.
602      * This method checks for the presence of separator characters in the given
603      * string. If multiple characters are found, the first one is assumed to be
604      * the correct separator. If there are quoting characters, they are taken
605      * into account, too.
606      *
607      * @param line the line to be checked
608      * @return the index of the separator character or -1 if none is found
609      */
610     private static int findSeparator(String line)
611     {
612         int index =
613                 findSeparatorBeforeQuote(line,
614                         findFirstOccurrence(line, QUOTE_CHARACTERS));
615         if (index < 0)
616         {
617             index = findFirstOccurrence(line, SEPARATOR_CHARS);
618         }
619         return index;
620     }
621 
622     /**
623      * Checks for the occurrence of the specified separators in the given line.
624      * The index of the first separator is returned.
625      *
626      * @param line the line to be investigated
627      * @param separators a string with the separator characters to look for
628      * @return the lowest index of a separator character or -1 if no separator
629      *         is found
630      */
631     private static int findFirstOccurrence(String line, String separators)
632     {
633         int index = -1;
634 
635         for (int i = 0; i < separators.length(); i++)
636         {
637             char sep = separators.charAt(i);
638             int pos = line.indexOf(sep);
639             if (pos >= 0)
640             {
641                 if (index < 0 || pos < index)
642                 {
643                     index = pos;
644                 }
645             }
646         }
647 
648         return index;
649     }
650 
651     /**
652      * Searches for a separator character directly before a quoting character.
653      * If the first non-whitespace character before a quote character is a
654      * separator, it is considered the "real" separator in this line - even if
655      * there are other separators before.
656      *
657      * @param line the line to be investigated
658      * @param quoteIndex the index of the quote character
659      * @return the index of the separator before the quote or &lt; 0 if there is
660      *         none
661      */
662     private static int findSeparatorBeforeQuote(String line, int quoteIndex)
663     {
664         int index = quoteIndex - 1;
665         while (index >= 0 && Character.isWhitespace(line.charAt(index)))
666         {
667             index--;
668         }
669 
670         if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0)
671         {
672             index = -1;
673         }
674 
675         return index;
676     }
677 
678     /**
679      * Add quotes around the specified value if it contains a comment character.
680      */
681     private String formatValue(String value)
682     {
683         boolean quoted = false;
684 
685         for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
686         {
687             char c = COMMENT_CHARS.charAt(i);
688             if (value.indexOf(c) != -1)
689             {
690                 quoted = true;
691             }
692         }
693 
694         if (quoted)
695         {
696             return '"' + value.replaceAll("\"", "\\\\\\\"") + '"';
697         }
698         else
699         {
700             return value;
701         }
702     }
703 
704     /**
705      * Determine if the given line is a comment line.
706      *
707      * @param line The line to check.
708      * @return true if the line is empty or starts with one of the comment
709      *         characters
710      */
711     protected boolean isCommentLine(String line)
712     {
713         if (line == null)
714         {
715             return false;
716         }
717         // blank lines are also treated as comment lines
718         return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
719     }
720 
721     /**
722      * Determine if the given line is a section.
723      *
724      * @param line The line to check.
725      * @return true if the line contains a section
726      */
727     protected boolean isSectionLine(String line)
728     {
729         if (line == null)
730         {
731             return false;
732         }
733         return line.startsWith("[") && line.endsWith("]");
734     }
735 
736     /**
737      * Return a set containing the sections in this ini configuration. Note that
738      * changes to this set do not affect the configuration.
739      *
740      * @return a set containing the sections.
741      */
742     public Set<String> getSections()
743     {
744         Set<String> sections = new LinkedHashSet<String>();
745         boolean globalSection = false;
746         boolean inSection = false;
747 
748         for (ConfigurationNode node : getRootNode().getChildren())
749         {
750             if (isSectionNode(node))
751             {
752                 inSection = true;
753                 sections.add(node.getName());
754             }
755             else
756             {
757                 if (!inSection && !globalSection)
758                 {
759                     globalSection = true;
760                     sections.add(null);
761                 }
762             }
763         }
764 
765         return sections;
766     }
767 
768     /**
769      * Returns a configuration with the content of the specified section. This
770      * provides an easy way of working with a single section only. The way this
771      * configuration is structured internally, this method is very similar to
772      * calling {@link HierarchicalConfiguration#configurationAt(String)} with
773      * the name of the section in question. There are the following differences
774      * however:
775      * <ul>
776      * <li>This method never throws an exception. If the section does not exist,
777      * it is created now. The configuration returned in this case is empty.</li>
778      * <li>If section is contained multiple times in the configuration, the
779      * configuration returned by this method is initialized with the first
780      * occurrence of the section. (This can only happen if
781      * {@code addProperty()} has been used in a way that does not conform
782      * to the storage scheme used by {@code HierarchicalINIConfiguration}.
783      * If used correctly, there will not be duplicate sections.)</li>
784      * <li>There is special support for the global section: Passing in
785      * <b>null</b> as section name returns a configuration with the content of
786      * the global section (which may also be empty).</li>
787      * </ul>
788      *
789      * @param name the name of the section in question; <b>null</b> represents
790      *        the global section
791      * @return a configuration containing only the properties of the specified
792      *         section
793      */
794     public SubnodeConfiguration getSection(String name)
795     {
796         if (name == null)
797         {
798             return getGlobalSection();
799         }
800 
801         else
802         {
803             try
804             {
805                 return configurationAt(name);
806             }
807             catch (IllegalArgumentException iex)
808             {
809                 // the passed in key does not map to exactly one node
810                 // obtain the node for the section, create it on demand
811                 return new SubnodeConfiguration(this, getSectionNode(name));
812             }
813         }
814     }
815 
816     /**
817      * Obtains the node representing the specified section. This method is
818      * called while the configuration is loaded. If a node for this section
819      * already exists, it is returned. Otherwise a new node is created.
820      *
821      * @param sectionName the name of the section
822      * @return the node for this section
823      */
824     private ConfigurationNode getSectionNode(String sectionName)
825     {
826         List<ConfigurationNode> nodes = getRootNode().getChildren(sectionName);
827         if (!nodes.isEmpty())
828         {
829             return nodes.get(0);
830         }
831 
832         ConfigurationNode node = createNode(sectionName);
833         markSectionNode(node);
834         getRootNode().addChild(node);
835         return node;
836     }
837 
838     /**
839      * Creates a sub configuration for the global section of the represented INI
840      * configuration.
841      *
842      * @return the sub configuration for the global section
843      */
844     private SubnodeConfiguration getGlobalSection()
845     {
846         ViewNode parent = new ViewNode();
847 
848         for (ConfigurationNode node : getRootNode().getChildren())
849         {
850             if (!isSectionNode(node))
851             {
852                 synchronized (node)
853                 {
854                     parent.addChild(node);
855                 }
856             }
857         }
858 
859         return createSubnodeConfiguration(parent);
860     }
861 
862     /**
863      * Marks a configuration node as a section node. This means that this node
864      * represents a section header. This implementation uses the node's
865      * reference property to store a flag.
866      *
867      * @param node the node to be marked
868      */
869     private static void markSectionNode(ConfigurationNode node)
870     {
871         node.setReference(Boolean.TRUE);
872     }
873 
874     /**
875      * Checks whether the specified configuration node represents a section.
876      *
877      * @param node the node in question
878      * @return a flag whether this node represents a section
879      */
880     private static boolean isSectionNode(ConfigurationNode node)
881     {
882         return node.getReference() != null || node.getChildrenCount() > 0;
883     }
884 }