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