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 */ 017package org.apache.commons.configuration; 018 019import java.io.BufferedReader; 020import java.io.File; 021import java.io.IOException; 022import java.io.PrintWriter; 023import java.io.Reader; 024import java.io.Writer; 025import java.net.URL; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Iterator; 029import java.util.LinkedHashSet; 030import java.util.List; 031import java.util.Set; 032 033import org.apache.commons.configuration.tree.ConfigurationNode; 034import org.apache.commons.configuration.tree.ViewNode; 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 typical ini file could look something like: 053 * </p> 054 * <pre> 055 * [section1] 056 * ; this is a comment! 057 * var1 = foo 058 * var2 = bar 059 * 060 * [section2] 061 * var1 = doo 062 * </pre> 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} 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 separator:</b> The ':' character is also accepted in place of 086 * '=' to separate keys and values in parameters, for example 087 * {@code var1 : foo}.</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} 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 */ 204public 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 * "value" ; comment -> value 437 * </pre> 438 * 439 * <pre> 440 * 'value' ; comment -> 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 < 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}