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.configuration2; 018 019import java.io.BufferedReader; 020import java.io.IOException; 021import java.io.PrintWriter; 022import java.io.Reader; 023import java.io.Writer; 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.LinkedHashMap; 027import java.util.LinkedHashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Set; 031 032import org.apache.commons.configuration2.convert.ListDelimiterHandler; 033import org.apache.commons.configuration2.ex.ConfigurationException; 034import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 035import org.apache.commons.configuration2.tree.ImmutableNode; 036import org.apache.commons.configuration2.tree.InMemoryNodeModel; 037import org.apache.commons.configuration2.tree.InMemoryNodeModelSupport; 038import org.apache.commons.configuration2.tree.NodeHandler; 039import org.apache.commons.configuration2.tree.NodeHandlerDecorator; 040import org.apache.commons.configuration2.tree.NodeSelector; 041import org.apache.commons.configuration2.tree.TrackedNodeModel; 042 043/** 044 * <p> 045 * A specialized hierarchical configuration implementation for parsing ini 046 * files. 047 * </p> 048 * <p> 049 * An initialization or ini file is a configuration file typically found on 050 * Microsoft's Windows operating system and contains data for Windows based 051 * applications. 052 * </p> 053 * <p> 054 * Although popularized by Windows, ini files can be used on any system or 055 * platform due to the fact that they are merely text files that can easily be 056 * parsed and modified by both humans and computers. 057 * </p> 058 * <p> 059 * A typical ini file could look something like: 060 * </p> 061 * <pre> 062 * [section1] 063 * ; this is a comment! 064 * var1 = foo 065 * var2 = bar 066 * 067 * [section2] 068 * var1 = doo 069 * </pre> 070 * <p> 071 * The format of ini files is fairly straight forward and is composed of three 072 * components:</p> 073 * <ul> 074 * <li><b>Sections:</b> Ini files are split into sections, each section starting 075 * with a section declaration. A section declaration starts with a '[' and ends 076 * with a ']'. Sections occur on one line only.</li> 077 * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters 078 * have a typical {@code key = value} format.</li> 079 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li> 080 * </ul> 081 * <p> 082 * There are various implementations of the ini file format by various vendors 083 * which has caused a number of differences to appear. As far as possible this 084 * configuration tries to be lenient and support most of the differences. 085 * </p> 086 * <p> 087 * Some of the differences supported are as follows: 088 * </p> 089 * <ul> 090 * <li><b>Comments:</b> The '#' character is also accepted as a comment 091 * signifier.</li> 092 * <li><b>Key value separator:</b> The ':' character is also accepted in place of 093 * '=' to separate keys and values in parameters, for example 094 * {@code var1 : foo}.</li> 095 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed, 096 * this configuration does however support this feature. In the event of a duplicate 097 * section, the two section's values are merged so that there is only a single 098 * section. <strong>Note</strong>: This also affects the internal data of the 099 * configuration. If it is saved, only a single section is written!</li> 100 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only 101 * allowed if they are in two different sections, thus they are local to 102 * sections; this configuration simply merges duplicates; if a section has a 103 * duplicate parameter the values are then added to the key as a list.</li> 104 * </ul> 105 * <p> 106 * Global parameters are also allowed; any parameters declared before a section 107 * is declared are added to a global section. It is important to note that this 108 * global section does not have a name. 109 * </p> 110 * <p> 111 * In all instances, a parameter's key is prepended with its section name and a 112 * '.' (period). Thus a parameter named "var1" in "section1" will have the key 113 * {@code section1.var1} in this configuration. (This is the default 114 * behavior. Because this is a hierarchical configuration you can change this by 115 * setting a different {@link org.apache.commons.configuration2.tree.ExpressionEngine}.) 116 * </p> 117 * <h3>Implementation Details:</h3> Consider the following ini file: 118 * <pre> 119 * default = ok 120 * 121 * [section1] 122 * var1 = foo 123 * var2 = doodle 124 * 125 * [section2] 126 * ; a comment 127 * var1 = baz 128 * var2 = shoodle 129 * bad = 130 * = worse 131 * 132 * [section3] 133 * # another comment 134 * var1 : foo 135 * var2 : bar 136 * var5 : test1 137 * 138 * [section3] 139 * var3 = foo 140 * var4 = bar 141 * var5 = test2 142 * 143 * [sectionSeparators] 144 * passwd : abc=def 145 * a:b = "value" 146 * </pre> 147 * <p> 148 * This ini file will be parsed without error. Note:</p> 149 * <ul> 150 * <li>The parameter named "default" is added to the global section, it's value 151 * is accessed simply using {@code getProperty("default")}.</li> 152 * <li>Section 1's parameters can be accessed using 153 * {@code getProperty("section1.var1")}.</li> 154 * <li>The parameter named "bad" simply adds the parameter with an empty value.</li> 155 * <li>The empty key with value "= worse" is added using a key consisting of a 156 * single space character. This key is still added to section 2 and the value 157 * can be accessed using {@code getProperty("section2. ")}, notice the 158 * period '.' and the space following the section name.</li> 159 * <li>Section three uses both '=' and ':' to separate keys and values.</li> 160 * <li>Section 3 has a duplicate key named "var5". The value for this key is 161 * [test1, test2], and is represented as a List.</li> 162 * <li>The section called <em>sectionSeparators</em> demonstrates how the 163 * configuration deals with multiple occurrences of separator characters. Per 164 * default the first separator character in a line is detected and used to 165 * split the key from the value. Therefore the first property definition in this 166 * section has the key {@code passwd} and the value {@code abc=def}. 167 * This default behavior can be changed by using quotes. If there is a separator 168 * character before the first quote character (ignoring whitespace), this 169 * character is used as separator. Thus the second property definition in the 170 * section has the key {@code a:b} and the value {@code value}.</li> 171 * </ul> 172 * <p> 173 * Internally, this configuration maps the content of the represented ini file 174 * to its node structure in the following way:</p> 175 * <ul> 176 * <li>Sections are represented by direct child nodes of the root node.</li> 177 * <li>For the content of a section, corresponding nodes are created as children 178 * of the section node.</li> 179 * </ul> 180 * <p> 181 * This explains how the keys for the properties can be constructed. You can 182 * also use other methods of {@link HierarchicalConfiguration} for querying or 183 * manipulating the hierarchy of configuration nodes, for instance the 184 * {@code configurationAt()} method for obtaining the data of a specific 185 * section. However, be careful that the storage scheme described above is not 186 * violated (e.g. by adding multiple levels of nodes or inserting duplicate 187 * section nodes). Otherwise, the special methods for ini configurations may not 188 * work correctly! 189 * </p> 190 * <p> 191 * The set of sections in this configuration can be retrieved using the 192 * {@code getSections()} method. For obtaining a 193 * {@code SubnodeConfiguration} with the content of a specific section the 194 * {@code getSection()} method can be used. 195 * </p> 196 * <p> 197 * Like other {@code Configuration} implementations, this class uses a 198 * {@code Synchronizer} object to control concurrent access. By choosing a 199 * suitable implementation of the {@code Synchronizer} interface, an instance 200 * can be made thread-safe or not. Note that access to most of the properties 201 * typically set through a builder is not protected by the {@code Synchronizer}. 202 * The intended usage is that these properties are set once at construction 203 * time through the builder and after that remain constant. If you wish to 204 * change such properties during life time of an instance, you have to use 205 * the {@code lock()} and {@code unlock()} methods manually to ensure that 206 * other threads see your changes. 207 * </p> 208 * <p> 209 * As this class extends {@link AbstractConfiguration}, all basic features 210 * like variable interpolation, list handling, or data type conversions are 211 * available as well. This is described in the chapter 212 * <a href="http://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html"> 213 * Basic features and AbstractConfiguration</a> of the user's guide. 214 * </p> 215 * <p> 216 * Note that this configuration does not support properties with null values. 217 * Such properties are considered to be section nodes. 218 * </p> 219 * 220 * @author <a 221 * href="http://commons.apache.org/configuration/team-list.html">Commons 222 * Configuration team</a> 223 * @version $Id: INIConfiguration.java 1806819 2017-08-31 16:03:06Z oheger $ 224 * @since 1.6 225 */ 226public class INIConfiguration extends BaseHierarchicalConfiguration implements 227 FileBasedConfiguration 228{ 229 /** 230 * The characters that signal the start of a comment line. 231 */ 232 protected static final String COMMENT_CHARS = "#;"; 233 234 /** 235 * The characters used to separate keys from values. 236 */ 237 protected static final String SEPARATOR_CHARS = "=:"; 238 239 /** 240 * Constant for the line separator. 241 */ 242 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 243 244 /** 245 * The characters used for quoting values. 246 */ 247 private static final String QUOTE_CHARACTERS = "\"'"; 248 249 /** 250 * The line continuation character. 251 */ 252 private static final String LINE_CONT = "\\"; 253 254 /** 255 * The separator used when writing an INI file. 256 */ 257 private String separatorUsedInOutput = " = "; 258 259 /** 260 * Create a new empty INI Configuration. 261 */ 262 public INIConfiguration() 263 { 264 super(); 265 } 266 267 /** 268 * Creates a new instance of {@code INIConfiguration} with the 269 * content of the specified {@code HierarchicalConfiguration}. 270 * 271 * @param c the configuration to be copied 272 * @since 2.0 273 */ 274 public INIConfiguration(HierarchicalConfiguration<ImmutableNode> c) 275 { 276 super(c); 277 } 278 279 /** 280 * Get separator used in INI output. see {@code setSeparatorUsedInOutput} 281 * for further explanation 282 * 283 * @return the current separator for writing the INI output 284 * @since 2.2 285 */ 286 public String getSeparatorUsedInOutput() 287 { 288 beginRead(false); 289 try 290 { 291 return separatorUsedInOutput; 292 } 293 finally 294 { 295 endRead(); 296 } 297 } 298 299 /** 300 * Allows setting the key and value separator which is used for the creation 301 * of the resulting INI output 302 * 303 * @param separator String of the new separator for INI output 304 * @since 2.2 305 */ 306 public void setSeparatorUsedInOutput(String separator) 307 { 308 beginWrite(false); 309 try 310 { 311 this.separatorUsedInOutput = separator; 312 } 313 finally 314 { 315 endWrite(); 316 } 317 } 318 319 /** 320 * Save the configuration to the specified writer. 321 * 322 * @param writer - The writer to save the configuration to. 323 * @throws ConfigurationException If an error occurs while writing the 324 * configuration 325 * @throws IOException if an I/O error occurs 326 */ 327 @Override 328 public void write(Writer writer) throws ConfigurationException, IOException 329 { 330 PrintWriter out = new PrintWriter(writer); 331 boolean first = true; 332 final String separator = getSeparatorUsedInOutput(); 333 334 beginRead(false); 335 try 336 { 337 for (ImmutableNode node : getModel().getNodeHandler().getRootNode() 338 .getChildren()) 339 { 340 if (isSectionNode(node)) 341 { 342 if (!first) 343 { 344 out.println(); 345 } 346 out.print("["); 347 out.print(node.getNodeName()); 348 out.print("]"); 349 out.println(); 350 351 for (ImmutableNode child : node.getChildren()) 352 { 353 writeProperty(out, child.getNodeName(), 354 child.getValue(), separator); 355 } 356 } 357 else 358 { 359 writeProperty(out, node.getNodeName(), node.getValue(), separator); 360 } 361 first = false; 362 } 363 out.println(); 364 out.flush(); 365 } 366 finally 367 { 368 endRead(); 369 } 370 } 371 372 /** 373 * Load the configuration from the given reader. Note that the 374 * {@code clear()} method is not called so the configuration read in will 375 * be merged with the current configuration. 376 * 377 * @param in the reader to read the configuration from. 378 * @throws ConfigurationException If an error occurs while reading the 379 * configuration 380 * @throws IOException if an I/O error occurs 381 */ 382 @Override 383 public void read(Reader in) throws ConfigurationException, IOException 384 { 385 BufferedReader bufferedReader = new BufferedReader(in); 386 Map<String, ImmutableNode.Builder> sectionBuilders = new LinkedHashMap<>(); 387 ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(); 388 389 createNodeBuilders(bufferedReader, rootBuilder, sectionBuilders); 390 ImmutableNode rootNode = createNewRootNode(rootBuilder, sectionBuilders); 391 addNodes(null, rootNode.getChildren()); 392 } 393 394 /** 395 * Creates a new root node from the builders constructed while reading the 396 * configuration file. 397 * 398 * @param rootBuilder the builder for the top-level section 399 * @param sectionBuilders a map storing the section builders 400 * @return the root node of the newly created hierarchy 401 */ 402 private static ImmutableNode createNewRootNode( 403 ImmutableNode.Builder rootBuilder, 404 Map<String, ImmutableNode.Builder> sectionBuilders) 405 { 406 for (Map.Entry<String, ImmutableNode.Builder> e : sectionBuilders 407 .entrySet()) 408 { 409 rootBuilder.addChild(e.getValue().name(e.getKey()).create()); 410 } 411 return rootBuilder.create(); 412 } 413 414 /** 415 * Reads the content of an INI file from the passed in reader and creates a 416 * structure of builders for constructing the {@code ImmutableNode} objects 417 * representing the data. 418 * 419 * @param in the reader 420 * @param rootBuilder the builder for the top-level section 421 * @param sectionBuilders a map storing the section builders 422 * @throws IOException if an I/O error occurs 423 */ 424 private void createNodeBuilders(BufferedReader in, 425 ImmutableNode.Builder rootBuilder, 426 Map<String, ImmutableNode.Builder> sectionBuilders) 427 throws IOException 428 { 429 ImmutableNode.Builder sectionBuilder = rootBuilder; 430 String line = in.readLine(); 431 while (line != null) 432 { 433 line = line.trim(); 434 if (!isCommentLine(line)) 435 { 436 if (isSectionLine(line)) 437 { 438 String section = line.substring(1, line.length() - 1); 439 sectionBuilder = sectionBuilders.get(section); 440 if (sectionBuilder == null) 441 { 442 sectionBuilder = new ImmutableNode.Builder(); 443 sectionBuilders.put(section, sectionBuilder); 444 } 445 } 446 447 else 448 { 449 String key; 450 String value = ""; 451 int index = findSeparator(line); 452 if (index >= 0) 453 { 454 key = line.substring(0, index); 455 value = parseValue(line.substring(index + 1), in); 456 } 457 else 458 { 459 key = line; 460 } 461 key = key.trim(); 462 if (key.length() < 1) 463 { 464 // use space for properties with no key 465 key = " "; 466 } 467 createValueNodes(sectionBuilder, key, value); 468 } 469 } 470 471 line = in.readLine(); 472 } 473 } 474 475 /** 476 * Creates the node(s) for the given key value-pair. If delimiter parsing is 477 * enabled, the value string is split if possible, and for each single value 478 * a node is created. Otherwise only a single node is added to the section. 479 * 480 * @param sectionBuilder the section builder for adding new nodes 481 * @param key the key 482 * @param value the value string 483 */ 484 private void createValueNodes(ImmutableNode.Builder sectionBuilder, 485 String key, String value) 486 { 487 Collection<String> values = 488 getListDelimiterHandler().split(value, false); 489 490 for (String v : values) 491 { 492 sectionBuilder.addChild(new ImmutableNode.Builder().name(key) 493 .value(v).create()); 494 } 495 } 496 497 /** 498 * Writes data about a property into the given stream. 499 * 500 * @param out the output stream 501 * @param key the key 502 * @param value the value 503 */ 504 private void writeProperty(PrintWriter out, String key, Object value, String separator) 505 { 506 out.print(key); 507 out.print(separator); 508 out.print(escapeValue(value.toString())); 509 out.println(); 510 } 511 512 /** 513 * Parse the value to remove the quotes and ignoring the comment. Example: 514 * 515 * <pre> 516 * "value" ; comment -> value 517 * </pre> 518 * 519 * <pre> 520 * 'value' ; comment -> value 521 * </pre> 522 * Note that a comment character is only recognized if there is at least one 523 * whitespace character before it. So it can appear in the property value, 524 * e.g.: 525 * <pre> 526 * C:\\Windows;C:\\Windows\\system32 527 * </pre> 528 * 529 * @param val the value to be parsed 530 * @param reader the reader (needed if multiple lines have to be read) 531 * @throws IOException if an IO error occurs 532 */ 533 private static String parseValue(String val, BufferedReader reader) throws IOException 534 { 535 StringBuilder propertyValue = new StringBuilder(); 536 boolean lineContinues; 537 String value = val.trim(); 538 539 do 540 { 541 boolean quoted = value.startsWith("\"") || value.startsWith("'"); 542 boolean stop = false; 543 boolean escape = false; 544 545 char quote = quoted ? value.charAt(0) : 0; 546 547 int i = quoted ? 1 : 0; 548 549 StringBuilder result = new StringBuilder(); 550 char lastChar = 0; 551 while (i < value.length() && !stop) 552 { 553 char c = value.charAt(i); 554 555 if (quoted) 556 { 557 if ('\\' == c && !escape) 558 { 559 escape = true; 560 } 561 else if (!escape && quote == c) 562 { 563 stop = true; 564 } 565 else if (escape && quote == c) 566 { 567 escape = false; 568 result.append(c); 569 } 570 else 571 { 572 if (escape) 573 { 574 escape = false; 575 result.append('\\'); 576 } 577 578 result.append(c); 579 } 580 } 581 else 582 { 583 if (isCommentChar(c) && Character.isWhitespace(lastChar)) 584 { 585 stop = true; 586 } 587 else 588 { 589 result.append(c); 590 } 591 } 592 593 i++; 594 lastChar = c; 595 } 596 597 String v = result.toString(); 598 if (!quoted) 599 { 600 v = v.trim(); 601 lineContinues = lineContinues(v); 602 if (lineContinues) 603 { 604 // remove trailing "\" 605 v = v.substring(0, v.length() - 1).trim(); 606 } 607 } 608 else 609 { 610 lineContinues = lineContinues(value, i); 611 } 612 propertyValue.append(v); 613 614 if (lineContinues) 615 { 616 propertyValue.append(LINE_SEPARATOR); 617 value = reader.readLine(); 618 } 619 } while (lineContinues && value != null); 620 621 return propertyValue.toString(); 622 } 623 624 /** 625 * Tests whether the specified string contains a line continuation marker. 626 * 627 * @param line the string to check 628 * @return a flag whether this line continues 629 */ 630 private static boolean lineContinues(String line) 631 { 632 String s = line.trim(); 633 return s.equals(LINE_CONT) 634 || (s.length() > 2 && s.endsWith(LINE_CONT) && Character 635 .isWhitespace(s.charAt(s.length() - 2))); 636 } 637 638 /** 639 * Tests whether the specified string contains a line continuation marker 640 * after the specified position. This method parses the string to remove a 641 * comment that might be present. Then it checks whether a line continuation 642 * marker can be found at the end. 643 * 644 * @param line the line to check 645 * @param pos the start position 646 * @return a flag whether this line continues 647 */ 648 private static boolean lineContinues(String line, int pos) 649 { 650 String s; 651 652 if (pos >= line.length()) 653 { 654 s = line; 655 } 656 else 657 { 658 int end = pos; 659 while (end < line.length() && !isCommentChar(line.charAt(end))) 660 { 661 end++; 662 } 663 s = line.substring(pos, end); 664 } 665 666 return lineContinues(s); 667 } 668 669 /** 670 * Tests whether the specified character is a comment character. 671 * 672 * @param c the character 673 * @return a flag whether this character starts a comment 674 */ 675 private static boolean isCommentChar(char c) 676 { 677 return COMMENT_CHARS.indexOf(c) >= 0; 678 } 679 680 /** 681 * Tries to find the index of the separator character in the given string. 682 * This method checks for the presence of separator characters in the given 683 * string. If multiple characters are found, the first one is assumed to be 684 * the correct separator. If there are quoting characters, they are taken 685 * into account, too. 686 * 687 * @param line the line to be checked 688 * @return the index of the separator character or -1 if none is found 689 */ 690 private static int findSeparator(String line) 691 { 692 int index = 693 findSeparatorBeforeQuote(line, 694 findFirstOccurrence(line, QUOTE_CHARACTERS)); 695 if (index < 0) 696 { 697 index = findFirstOccurrence(line, SEPARATOR_CHARS); 698 } 699 return index; 700 } 701 702 /** 703 * Checks for the occurrence of the specified separators in the given line. 704 * The index of the first separator is returned. 705 * 706 * @param line the line to be investigated 707 * @param separators a string with the separator characters to look for 708 * @return the lowest index of a separator character or -1 if no separator 709 * is found 710 */ 711 private static int findFirstOccurrence(String line, String separators) 712 { 713 int index = -1; 714 715 for (int i = 0; i < separators.length(); i++) 716 { 717 char sep = separators.charAt(i); 718 int pos = line.indexOf(sep); 719 if (pos >= 0) 720 { 721 if (index < 0 || pos < index) 722 { 723 index = pos; 724 } 725 } 726 } 727 728 return index; 729 } 730 731 /** 732 * Searches for a separator character directly before a quoting character. 733 * If the first non-whitespace character before a quote character is a 734 * separator, it is considered the "real" separator in this line - even if 735 * there are other separators before. 736 * 737 * @param line the line to be investigated 738 * @param quoteIndex the index of the quote character 739 * @return the index of the separator before the quote or < 0 if there is 740 * none 741 */ 742 private static int findSeparatorBeforeQuote(String line, int quoteIndex) 743 { 744 int index = quoteIndex - 1; 745 while (index >= 0 && Character.isWhitespace(line.charAt(index))) 746 { 747 index--; 748 } 749 750 if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0) 751 { 752 index = -1; 753 } 754 755 return index; 756 } 757 758 /** 759 * Escapes the given property value before it is written. This method add 760 * quotes around the specified value if it contains a comment character and 761 * handles list delimiter characters. 762 * 763 * @param value the string to be escaped 764 */ 765 private String escapeValue(String value) 766 { 767 return String.valueOf(getListDelimiterHandler().escape( 768 escapeComments(value), ListDelimiterHandler.NOOP_TRANSFORMER)); 769 } 770 771 /** 772 * Escapes comment characters in the given value. 773 * 774 * @param value the value to be escaped 775 * @return the value with comment characters escaped 776 */ 777 private static String escapeComments(String value) 778 { 779 boolean quoted = false; 780 781 for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++) 782 { 783 char c = COMMENT_CHARS.charAt(i); 784 if (value.indexOf(c) != -1) 785 { 786 quoted = true; 787 } 788 } 789 790 if (quoted) 791 { 792 return '"' + value.replaceAll("\"", "\\\\\\\"") + '"'; 793 } 794 else 795 { 796 return value; 797 } 798 } 799 800 /** 801 * Determine if the given line is a comment line. 802 * 803 * @param line The line to check. 804 * @return true if the line is empty or starts with one of the comment 805 * characters 806 */ 807 protected boolean isCommentLine(String line) 808 { 809 if (line == null) 810 { 811 return false; 812 } 813 // blank lines are also treated as comment lines 814 return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0; 815 } 816 817 /** 818 * Determine if the given line is a section. 819 * 820 * @param line The line to check. 821 * @return true if the line contains a section 822 */ 823 protected boolean isSectionLine(String line) 824 { 825 if (line == null) 826 { 827 return false; 828 } 829 return line.startsWith("[") && line.endsWith("]"); 830 } 831 832 /** 833 * Return a set containing the sections in this ini configuration. Note that 834 * changes to this set do not affect the configuration. 835 * 836 * @return a set containing the sections. 837 */ 838 public Set<String> getSections() 839 { 840 Set<String> sections = new LinkedHashSet<>(); 841 boolean globalSection = false; 842 boolean inSection = false; 843 844 beginRead(false); 845 try 846 { 847 for (ImmutableNode node : getModel().getNodeHandler().getRootNode() 848 .getChildren()) 849 { 850 if (isSectionNode(node)) 851 { 852 inSection = true; 853 sections.add(node.getNodeName()); 854 } 855 else 856 { 857 if (!inSection && !globalSection) 858 { 859 globalSection = true; 860 sections.add(null); 861 } 862 } 863 } 864 } 865 finally 866 { 867 endRead(); 868 } 869 870 return sections; 871 } 872 873 /** 874 * Returns a configuration with the content of the specified section. This 875 * provides an easy way of working with a single section only. The way this 876 * configuration is structured internally, this method is very similar to 877 * calling {@link HierarchicalConfiguration#configurationAt(String)} with 878 * the name of the section in question. There are the following differences 879 * however: 880 * <ul> 881 * <li>This method never throws an exception. If the section does not exist, 882 * it is created now. The configuration returned in this case is empty.</li> 883 * <li>If section is contained multiple times in the configuration, the 884 * configuration returned by this method is initialized with the first 885 * occurrence of the section. (This can only happen if 886 * {@code addProperty()} has been used in a way that does not conform 887 * to the storage scheme used by {@code INIConfiguration}. 888 * If used correctly, there will not be duplicate sections.)</li> 889 * <li>There is special support for the global section: Passing in 890 * <b>null</b> as section name returns a configuration with the content of 891 * the global section (which may also be empty).</li> 892 * </ul> 893 * 894 * @param name the name of the section in question; <b>null</b> represents 895 * the global section 896 * @return a configuration containing only the properties of the specified 897 * section 898 */ 899 public SubnodeConfiguration getSection(String name) 900 { 901 if (name == null) 902 { 903 return getGlobalSection(); 904 } 905 906 else 907 { 908 try 909 { 910 return (SubnodeConfiguration) configurationAt(name, true); 911 } 912 catch (ConfigurationRuntimeException iex) 913 { 914 // the passed in key does not map to exactly one node 915 // obtain the node for the section, create it on demand 916 InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 917 NodeSelector selector = parentModel.trackChildNodeWithCreation(null, name, this); 918 return createSubConfigurationForTrackedNode(selector, this); 919 } 920 } 921 } 922 923 /** 924 * Creates a sub configuration for the global section of the represented INI 925 * configuration. 926 * 927 * @return the sub configuration for the global section 928 */ 929 private SubnodeConfiguration getGlobalSection() 930 { 931 InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 932 NodeSelector selector = new NodeSelector(null); // selects parent 933 parentModel.trackNode(selector, this); 934 GlobalSectionNodeModel model = 935 new GlobalSectionNodeModel(this, selector); 936 SubnodeConfiguration sub = new SubnodeConfiguration(this, model); 937 initSubConfigurationForThisParent(sub); 938 return sub; 939 } 940 941 /** 942 * Checks whether the specified configuration node represents a section. 943 * 944 * @param node the node in question 945 * @return a flag whether this node represents a section 946 */ 947 private static boolean isSectionNode(ImmutableNode node) 948 { 949 return node.getValue() == null; 950 } 951 952 /** 953 * A specialized node model implementation for the sub configuration 954 * representing the global section of the INI file. This is a regular 955 * {@code TrackedNodeModel} with one exception: The {@code NodeHandler} used 956 * by this model applies a filter on the children of the root node so that 957 * only nodes are visible that are no sub sections. 958 */ 959 private static class GlobalSectionNodeModel extends TrackedNodeModel 960 { 961 /** 962 * Creates a new instance of {@code GlobalSectionNodeModel} and 963 * initializes it with the given underlying model. 964 * 965 * @param modelSupport the underlying {@code InMemoryNodeModel} 966 * @param selector the {@code NodeSelector} 967 */ 968 public GlobalSectionNodeModel(InMemoryNodeModelSupport modelSupport, 969 NodeSelector selector) 970 { 971 super(modelSupport, selector, true); 972 } 973 974 @Override 975 public NodeHandler<ImmutableNode> getNodeHandler() 976 { 977 return new NodeHandlerDecorator<ImmutableNode>() 978 { 979 @Override 980 public List<ImmutableNode> getChildren(ImmutableNode node) 981 { 982 List<ImmutableNode> children = super.getChildren(node); 983 return filterChildrenOfGlobalSection(node, children); 984 } 985 986 @Override 987 public List<ImmutableNode> getChildren(ImmutableNode node, 988 String name) 989 { 990 List<ImmutableNode> children = 991 super.getChildren(node, name); 992 return filterChildrenOfGlobalSection(node, children); 993 } 994 995 @Override 996 public int getChildrenCount(ImmutableNode node, String name) 997 { 998 List<ImmutableNode> children = 999 (name != null) ? super.getChildren(node, name) 1000 : super.getChildren(node); 1001 return filterChildrenOfGlobalSection(node, children).size(); 1002 } 1003 1004 @Override 1005 public ImmutableNode getChild(ImmutableNode node, int index) 1006 { 1007 List<ImmutableNode> children = super.getChildren(node); 1008 return filterChildrenOfGlobalSection(node, children).get( 1009 index); 1010 } 1011 1012 @Override 1013 public int indexOfChild(ImmutableNode parent, 1014 ImmutableNode child) 1015 { 1016 List<ImmutableNode> children = super.getChildren(parent); 1017 return filterChildrenOfGlobalSection(parent, children) 1018 .indexOf(child); 1019 } 1020 1021 @Override 1022 protected NodeHandler<ImmutableNode> getDecoratedNodeHandler() 1023 { 1024 return GlobalSectionNodeModel.super.getNodeHandler(); 1025 } 1026 1027 /** 1028 * Filters the child nodes of the global section. This method 1029 * checks whether the passed in node is the root node of the 1030 * configuration. If so, from the list of children all nodes are 1031 * filtered which are section nodes. 1032 * 1033 * @param node the node in question 1034 * @param children the children of this node 1035 * @return a list with the filtered children 1036 */ 1037 private List<ImmutableNode> filterChildrenOfGlobalSection( 1038 ImmutableNode node, List<ImmutableNode> children) 1039 { 1040 List<ImmutableNode> filteredList; 1041 if (node == getRootNode()) 1042 { 1043 filteredList = 1044 new ArrayList<>(children.size()); 1045 for (ImmutableNode child : children) 1046 { 1047 if (!isSectionNode(child)) 1048 { 1049 filteredList.add(child); 1050 } 1051 } 1052 } 1053 else 1054 { 1055 filteredList = children; 1056 } 1057 1058 return filteredList; 1059 } 1060 }; 1061 } 1062 } 1063}