1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.configuration2; 18 19 import java.io.BufferedReader; 20 import java.io.IOException; 21 import java.io.PrintWriter; 22 import java.io.Reader; 23 import java.io.Writer; 24 import java.util.LinkedHashMap; 25 import java.util.LinkedHashSet; 26 import java.util.List; 27 import java.util.Map; 28 import java.util.Set; 29 import java.util.stream.Collectors; 30 31 import org.apache.commons.configuration2.convert.ListDelimiterHandler; 32 import org.apache.commons.configuration2.ex.ConfigurationException; 33 import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 34 import org.apache.commons.configuration2.tree.ImmutableNode; 35 import org.apache.commons.configuration2.tree.InMemoryNodeModel; 36 import org.apache.commons.configuration2.tree.InMemoryNodeModelSupport; 37 import org.apache.commons.configuration2.tree.NodeHandler; 38 import org.apache.commons.configuration2.tree.NodeHandlerDecorator; 39 import org.apache.commons.configuration2.tree.NodeSelector; 40 import org.apache.commons.configuration2.tree.TrackedNodeModel; 41 42 /** 43 * <p> 44 * A specialized hierarchical configuration implementation for parsing ini files. 45 * </p> 46 * <p> 47 * An initialization or ini file is a configuration file typically found on Microsoft's Windows operating system and 48 * contains data for Windows based applications. 49 * </p> 50 * <p> 51 * Although popularized by Windows, ini files can be used on any system or platform due to the fact that they are merely 52 * text files that can easily be parsed and modified by both humans and computers. 53 * </p> 54 * <p> 55 * A typical ini file could look something like: 56 * </p> 57 * 58 * <pre> 59 * [section1] 60 * ; this is a comment! 61 * var1 = foo 62 * var2 = bar 63 * 64 * [section2] 65 * var1 = doo 66 * </pre> 67 * <p> 68 * The format of ini files is fairly straight forward and is composed of three components: 69 * </p> 70 * <ul> 71 * <li><b>Sections:</b> Ini files are split into sections, each section starting with a section declaration. A section 72 * declaration starts with a '[' and ends with a ']'. Sections occur on one line only.</li> 73 * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters have a typical {@code key = value} 74 * format.</li> 75 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li> 76 * </ul> 77 * <p> 78 * There are various implementations of the ini file format by various vendors which has caused a number of differences 79 * to appear. As far as possible this configuration tries to be lenient and support most of the differences. 80 * </p> 81 * <p> 82 * Some of the differences supported are as follows: 83 * </p> 84 * <ul> 85 * <li><b>Comments:</b> The '#' character is also accepted as a comment signifier.</li> 86 * <li><b>Key value separator:</b> The ':' character is also accepted in place of '=' to separate keys and values in 87 * parameters, for example {@code var1 : foo}.</li> 88 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed, this configuration does however support 89 * this feature. In the event of a duplicate section, the two section's values are merged so that there is only a single 90 * section. <strong>Note</strong>: This also affects the internal data of the configuration. If it is saved, only a 91 * single section is written!</li> 92 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only allowed if they are in two different 93 * sections, thus they are local to sections; this configuration simply merges duplicates; if a section has a duplicate 94 * parameter the values are then added to the key as a list.</li> 95 * </ul> 96 * <p> 97 * Global parameters are also allowed; any parameters declared before a section is declared are added to a global 98 * section. It is important to note that this global section does not have a name. 99 * </p> 100 * <p> 101 * In all instances, a parameter's key is prepended with its section name and a '.' (period). Thus a parameter named 102 * "var1" in "section1" will have the key {@code section1.var1} in this configuration. (This is the default behavior. 103 * Because this is a hierarchical configuration you can change this by setting a different 104 * {@link org.apache.commons.configuration2.tree.ExpressionEngine}.) 105 * </p> 106 * <p> 107 * <strong>Implementation Details:</strong> Consider the following ini file: 108 * </p> 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 */ 199 public class INIConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration { 200 201 /** 202 * The default characters that signal the start of a comment line. 203 */ 204 protected static final String COMMENT_CHARS = "#;"; 205 206 /** 207 * The default characters used to separate keys from values. 208 */ 209 protected static final String SEPARATOR_CHARS = "=:"; 210 211 /** 212 * Constant for the line separator. 213 */ 214 private static final String LINE_SEPARATOR = System.lineSeparator(); 215 216 /** 217 * The characters used for quoting values. 218 */ 219 private static final String QUOTE_CHARACTERS = "\"'"; 220 221 /** 222 * The line continuation character. 223 */ 224 private static final String LINE_CONT = "\\"; 225 226 /** 227 * The separator used when writing an INI file. 228 */ 229 private String separatorUsedInOutput = " = "; 230 231 /** 232 * The separator used when reading an INI file. 233 */ 234 private String separatorUsedInInput = SEPARATOR_CHARS; 235 236 /** 237 * The characters used to separate keys from values when reading an INI file. 238 */ 239 private String commentCharsUsedInInput = COMMENT_CHARS; 240 241 /** 242 * The flag for decision, whether inline comments on the section line are allowed. 243 */ 244 private boolean sectionInLineCommentsAllowed; 245 246 /** 247 * Create a new empty INI Configuration. 248 */ 249 public INIConfiguration() { 250 } 251 252 /** 253 * Creates a new instance of {@code INIConfiguration} with the content of the specified 254 * {@code HierarchicalConfiguration}. 255 * 256 * @param c the configuration to be copied 257 * @since 2.0 258 */ 259 public INIConfiguration(final HierarchicalConfiguration<ImmutableNode> c) { 260 super(c); 261 } 262 263 /** 264 * Create a new empty INI Configuration with option to allow inline comments on the section line. 265 * 266 * @param sectionInLineCommentsAllowed when true inline comments on the section line are allowed 267 */ 268 private INIConfiguration(final boolean sectionInLineCommentsAllowed) { 269 this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed; 270 } 271 272 /** 273 * Creates a new builder. 274 * 275 * @return a new builder. 276 * @since 2.9.0 277 */ 278 public static Builder builder() { 279 return new Builder(); 280 } 281 282 /** 283 * Builds instances of INIConfiguration. 284 * 285 * @since 2.9.0 286 */ 287 public static class Builder { 288 289 /** 290 * The flag for decision, whether inline comments on the section line are allowed. 291 */ 292 private boolean sectionInLineCommentsAllowed; 293 294 public Builder setSectionInLineCommentsAllowed(final boolean sectionInLineCommentsAllowed) { 295 this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed; 296 return this; 297 } 298 299 public INIConfiguration build() { 300 return new INIConfiguration(sectionInLineCommentsAllowed); 301 } 302 303 } 304 305 /** 306 * Gets separator used in INI output. see {@code setSeparatorUsedInOutput} for further explanation 307 * 308 * @return the current separator for writing the INI output 309 * @since 2.2 310 */ 311 public String getSeparatorUsedInOutput() { 312 beginRead(false); 313 try { 314 return separatorUsedInOutput; 315 } finally { 316 endRead(); 317 } 318 } 319 320 /** 321 * Allows setting the key and value separator which is used for the creation of the resulting INI output 322 * 323 * @param separator String of the new separator for INI output 324 * @since 2.2 325 */ 326 public void setSeparatorUsedInOutput(final String separator) { 327 beginWrite(false); 328 try { 329 this.separatorUsedInOutput = separator; 330 } finally { 331 endWrite(); 332 } 333 } 334 335 /** 336 * Gets separator used in INI reading. see {@code setSeparatorUsedInInput} for further explanation 337 * 338 * @return the current separator for reading the INI input 339 * @since 2.5 340 */ 341 public String getSeparatorUsedInInput() { 342 beginRead(false); 343 try { 344 return separatorUsedInInput; 345 } finally { 346 endRead(); 347 } 348 } 349 350 /** 351 * Allows setting the key and value separator which is used in reading an INI file 352 * 353 * @param separator String of the new separator for INI reading 354 * @since 2.5 355 */ 356 public void setSeparatorUsedInInput(final String separator) { 357 beginRead(false); 358 try { 359 this.separatorUsedInInput = separator; 360 } finally { 361 endRead(); 362 } 363 } 364 365 /** 366 * Gets comment leading separator used in INI reading. see {@code setCommentLeadingCharsUsedInInput} for further 367 * explanation 368 * 369 * @return the current separator for reading the INI input 370 * @since 2.5 371 */ 372 public String getCommentLeadingCharsUsedInInput() { 373 beginRead(false); 374 try { 375 return commentCharsUsedInInput; 376 } finally { 377 endRead(); 378 } 379 } 380 381 /** 382 * Allows setting the leading comment separator which is used in reading an INI file 383 * 384 * @param separator String of the new separator for INI reading 385 * @since 2.5 386 */ 387 public void setCommentLeadingCharsUsedInInput(final String separator) { 388 beginRead(false); 389 try { 390 this.commentCharsUsedInInput = separator; 391 } finally { 392 endRead(); 393 } 394 } 395 396 /** 397 * Save the configuration to the specified writer. 398 * 399 * @param writer - The writer to save the configuration to. 400 * @throws ConfigurationException If an error occurs while writing the configuration 401 * @throws IOException if an I/O error occurs. 402 */ 403 @Override 404 public void write(final Writer writer) throws ConfigurationException, IOException { 405 final PrintWriter out = new PrintWriter(writer); 406 boolean first = true; 407 final String separator = getSeparatorUsedInOutput(); 408 409 beginRead(false); 410 try { 411 for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) { 412 if (isSectionNode(node)) { 413 if (!first) { 414 out.println(); 415 } 416 out.print("["); 417 out.print(node.getNodeName()); 418 out.print("]"); 419 out.println(); 420 421 node.forEach(child -> writeProperty(out, child.getNodeName(), child.getValue(), separator)); 422 } else { 423 writeProperty(out, node.getNodeName(), node.getValue(), separator); 424 } 425 first = false; 426 } 427 out.println(); 428 out.flush(); 429 } finally { 430 endRead(); 431 } 432 } 433 434 /** 435 * Load the configuration from the given reader. Note that the {@code clear()} method is not called so the configuration 436 * read in will be merged with the current configuration. 437 * 438 * @param in the reader to read the configuration from. 439 * @throws ConfigurationException If an error occurs while reading the configuration 440 * @throws IOException if an I/O error occurs. 441 */ 442 @Override 443 public void read(final Reader in) throws ConfigurationException, IOException { 444 final BufferedReader bufferedReader = new BufferedReader(in); 445 final Map<String, ImmutableNode.Builder> sectionBuilders = new LinkedHashMap<>(); 446 final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(); 447 448 createNodeBuilders(bufferedReader, rootBuilder, sectionBuilders); 449 final ImmutableNode rootNode = createNewRootNode(rootBuilder, sectionBuilders); 450 addNodes(null, rootNode.getChildren()); 451 } 452 453 /** 454 * Creates a new root node from the builders constructed while reading the configuration file. 455 * 456 * @param rootBuilder the builder for the top-level section 457 * @param sectionBuilders a map storing the section builders 458 * @return the root node of the newly created hierarchy 459 */ 460 private static ImmutableNode createNewRootNode(final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders) { 461 sectionBuilders.forEach((k, v) -> rootBuilder.addChild(v.name(k).create())); 462 return rootBuilder.create(); 463 } 464 465 /** 466 * Reads the content of an INI file from the passed in reader and creates a structure of builders for constructing the 467 * {@code ImmutableNode} objects representing the data. 468 * 469 * @param in the reader 470 * @param rootBuilder the builder for the top-level section 471 * @param sectionBuilders a map storing the section builders 472 * @throws IOException if an I/O error occurs. 473 */ 474 private void createNodeBuilders(final BufferedReader in, final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders) 475 throws IOException { 476 ImmutableNode.Builder sectionBuilder = rootBuilder; 477 String line = in.readLine(); 478 while (line != null) { 479 line = line.trim(); 480 if (!isCommentLine(line)) { 481 if (isSectionLine(line)) { 482 final int length = sectionInLineCommentsAllowed ? line.indexOf("]") : line.length() - 1; 483 final String section = line.substring(1, length); 484 sectionBuilder = sectionBuilders.computeIfAbsent(section, k -> new ImmutableNode.Builder()); 485 } else { 486 String key; 487 String value = ""; 488 final int index = findSeparator(line); 489 if (index >= 0) { 490 key = line.substring(0, index); 491 value = parseValue(line.substring(index + 1), in); 492 } else { 493 key = line; 494 } 495 key = key.trim(); 496 if (key.isEmpty()) { 497 // use space for properties with no key 498 key = " "; 499 } 500 createValueNodes(sectionBuilder, key, value); 501 } 502 } 503 504 line = in.readLine(); 505 } 506 } 507 508 /** 509 * Creates the node(s) for the given key value-pair. If delimiter parsing is enabled, the value string is split if 510 * possible, and for each single value a node is created. Otherwise only a single node is added to the section. 511 * 512 * @param sectionBuilder the section builder for adding new nodes 513 * @param key the key 514 * @param value the value string 515 */ 516 private void createValueNodes(final ImmutableNode.Builder sectionBuilder, final String key, final String value) { 517 getListDelimiterHandler().split(value, false).forEach(v -> sectionBuilder.addChild(new ImmutableNode.Builder().name(key).value(v).create())); 518 } 519 520 /** 521 * Writes data about a property into the given stream. 522 * 523 * @param out the output stream 524 * @param key the key 525 * @param value the value 526 */ 527 private void writeProperty(final PrintWriter out, final String key, final Object value, final String separator) { 528 out.print(key); 529 out.print(separator); 530 out.print(escapeValue(value.toString())); 531 out.println(); 532 } 533 534 /** 535 * Parse the value to remove the quotes and ignoring the comment. Example: 536 * 537 * <pre> 538 * "value" ; comment -> value 539 * </pre> 540 * 541 * <pre> 542 * 'value' ; comment -> value 543 * </pre> 544 * 545 * Note that a comment character is only recognized if there is at least one whitespace character before it. So it can 546 * appear in the property value, e.g.: 547 * 548 * <pre> 549 * C:\\Windows;C:\\Windows\\system32 550 * </pre> 551 * 552 * @param val the value to be parsed 553 * @param reader the reader (needed if multiple lines have to be read) 554 * @throws IOException if an IO error occurs 555 */ 556 private String parseValue(final String val, final BufferedReader reader) throws IOException { 557 final StringBuilder propertyValue = new StringBuilder(); 558 boolean lineContinues; 559 String value = val.trim(); 560 561 do { 562 final boolean quoted = value.startsWith("\"") || value.startsWith("'"); 563 boolean stop = false; 564 boolean escape = false; 565 566 final char quote = quoted ? value.charAt(0) : 0; 567 568 int i = quoted ? 1 : 0; 569 570 final StringBuilder result = new StringBuilder(); 571 char lastChar = 0; 572 while (i < value.length() && !stop) { 573 final char c = value.charAt(i); 574 575 if (quoted) { 576 if ('\\' == c && !escape) { 577 escape = true; 578 } else if (!escape && quote == c) { 579 stop = true; 580 } else { 581 if (escape && quote == c) { 582 escape = false; 583 } else if (escape) { 584 escape = false; 585 result.append('\\'); 586 } 587 result.append(c); 588 } 589 } else if (isCommentChar(c) && Character.isWhitespace(lastChar)) { 590 stop = true; 591 } else { 592 result.append(c); 593 } 594 595 i++; 596 lastChar = c; 597 } 598 599 String v = result.toString(); 600 if (!quoted) { 601 v = v.trim(); 602 lineContinues = lineContinues(v); 603 if (lineContinues) { 604 // remove trailing "\" 605 v = v.substring(0, v.length() - 1).trim(); 606 } 607 } else { 608 lineContinues = lineContinues(value, i); 609 } 610 propertyValue.append(v); 611 612 if (lineContinues) { 613 propertyValue.append(LINE_SEPARATOR); 614 value = reader.readLine(); 615 } 616 } while (lineContinues && value != null); 617 618 return propertyValue.toString(); 619 } 620 621 /** 622 * Tests whether the specified string contains a line continuation marker. 623 * 624 * @param line the string to check 625 * @return a flag whether this line continues 626 */ 627 private static boolean lineContinues(final String line) { 628 final String s = line.trim(); 629 return s.equals(LINE_CONT) || s.length() > 2 && s.endsWith(LINE_CONT) && Character.isWhitespace(s.charAt(s.length() - 2)); 630 } 631 632 /** 633 * Tests whether the specified string contains a line continuation marker after the specified position. This method 634 * parses the string to remove a comment that might be present. Then it checks whether a line continuation marker can be 635 * found at the end. 636 * 637 * @param line the line to check 638 * @param pos the start position 639 * @return a flag whether this line continues 640 */ 641 private boolean lineContinues(final String line, final int pos) { 642 final String s; 643 644 if (pos >= line.length()) { 645 s = line; 646 } else { 647 int end = pos; 648 while (end < line.length() && !isCommentChar(line.charAt(end))) { 649 end++; 650 } 651 s = line.substring(pos, end); 652 } 653 654 return lineContinues(s); 655 } 656 657 /** 658 * Tests whether the specified character is a comment character. 659 * 660 * @param c the character 661 * @return a flag whether this character starts a comment 662 */ 663 private boolean isCommentChar(final char c) { 664 return getCommentLeadingCharsUsedInInput().indexOf(c) >= 0; 665 } 666 667 /** 668 * Tries to find the index of the separator character in the given string. This method checks for the presence of 669 * separator characters in the given string. If multiple characters are found, the first one is assumed to be the 670 * correct separator. If there are quoting characters, they are taken into account, too. 671 * 672 * @param line the line to be checked 673 * @return the index of the separator character or -1 if none is found 674 */ 675 private int findSeparator(final String line) { 676 int index = findSeparatorBeforeQuote(line, findFirstOccurrence(line, QUOTE_CHARACTERS)); 677 if (index < 0) { 678 index = findFirstOccurrence(line, getSeparatorUsedInInput()); 679 } 680 return index; 681 } 682 683 /** 684 * Checks for the occurrence of the specified separators in the given line. The index of the first separator is 685 * returned. 686 * 687 * @param line the line to be investigated 688 * @param separators a string with the separator characters to look for 689 * @return the lowest index of a separator character or -1 if no separator is found 690 */ 691 private static int findFirstOccurrence(final String line, final String separators) { 692 int index = -1; 693 694 for (int i = 0; i < separators.length(); i++) { 695 final char sep = separators.charAt(i); 696 final int pos = line.indexOf(sep); 697 if (pos >= 0 && (index < 0 || pos < index)) { 698 index = pos; 699 } 700 } 701 702 return index; 703 } 704 705 /** 706 * Searches for a separator character directly before a quoting character. If the first non-whitespace character before 707 * a quote character is a separator, it is considered the "real" separator in this line - even if there are other 708 * separators before. 709 * 710 * @param line the line to be investigated 711 * @param quoteIndex the index of the quote character 712 * @return the index of the separator before the quote or < 0 if there is none 713 */ 714 private static int findSeparatorBeforeQuote(final String line, final int quoteIndex) { 715 int index = quoteIndex - 1; 716 while (index >= 0 && Character.isWhitespace(line.charAt(index))) { 717 index--; 718 } 719 720 if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0) { 721 index = -1; 722 } 723 724 return index; 725 } 726 727 /** 728 * Escapes the given property value before it is written. This method add quotes around the specified value if it 729 * contains a comment character and handles list delimiter characters. 730 * 731 * @param value the string to be escaped 732 */ 733 private String escapeValue(final String value) { 734 return String.valueOf(getListDelimiterHandler().escape(escapeComments(value), ListDelimiterHandler.NOOP_TRANSFORMER)); 735 } 736 737 /** 738 * Escapes comment characters in the given value. 739 * 740 * @param value the value to be escaped 741 * @return the value with comment characters escaped 742 */ 743 private String escapeComments(final String value) { 744 final String commentChars = getCommentLeadingCharsUsedInInput(); 745 boolean quoted = false; 746 747 for (int i = 0; i < commentChars.length(); i++) { 748 final char c = commentChars.charAt(i); 749 if (value.indexOf(c) != -1) { 750 quoted = true; 751 break; 752 } 753 } 754 755 if (quoted) { 756 return '"' + value.replace("\"", "\\\"") + '"'; 757 } 758 return value; 759 } 760 761 /** 762 * Determine if the given line is a comment line. 763 * 764 * @param line The line to check. 765 * @return true if the line is empty or starts with one of the comment characters 766 */ 767 protected boolean isCommentLine(final String line) { 768 if (line == null) { 769 return false; 770 } 771 // blank lines are also treated as comment lines 772 return line.isEmpty() || getCommentLeadingCharsUsedInInput().indexOf(line.charAt(0)) >= 0; 773 } 774 775 /** 776 * Determine if the given line is a section. 777 * 778 * @param line The line to check. 779 * @return true if the line contains a section 780 */ 781 protected boolean isSectionLine(final String line) { 782 if (line == null) { 783 return false; 784 } 785 return sectionInLineCommentsAllowed ? isNonStrictSection(line) : isStrictSection(line); 786 } 787 788 /** 789 * Determine if the entire given line is a section - inline comments are not allowed. 790 * 791 * @param line The line to check. 792 * @return true if the entire line is a section 793 */ 794 private static boolean isStrictSection(final String line) { 795 return line.startsWith("[") && line.endsWith("]"); 796 } 797 798 /** 799 * Determine if the given line contains a section - inline comments are allowed. 800 * 801 * @param line The line to check. 802 * @return true if the line contains a section 803 */ 804 private static boolean isNonStrictSection(final String line) { 805 return line.startsWith("[") && line.contains("]"); 806 } 807 808 /** 809 * Gets a set containing the sections in this ini configuration. Note that changes to this set do not affect the 810 * configuration. 811 * 812 * @return a set containing the sections. 813 */ 814 public Set<String> getSections() { 815 final Set<String> sections = new LinkedHashSet<>(); 816 boolean globalSection = false; 817 boolean inSection = false; 818 819 beginRead(false); 820 try { 821 for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) { 822 if (isSectionNode(node)) { 823 inSection = true; 824 sections.add(node.getNodeName()); 825 } else if (!inSection && !globalSection) { 826 globalSection = true; 827 sections.add(null); 828 } 829 } 830 } finally { 831 endRead(); 832 } 833 834 return sections; 835 } 836 837 /** 838 * Gets a configuration with the content of the specified section. This provides an easy way of working with a single 839 * section only. The way this configuration is structured internally, this method is very similar to calling 840 * {@link HierarchicalConfiguration#configurationAt(String)} with the name of the section in question. There are the 841 * following differences however: 842 * <ul> 843 * <li>This method never throws an exception. If the section does not exist, it is created now. The configuration 844 * returned in this case is empty.</li> 845 * <li>If section is contained multiple times in the configuration, the configuration returned by this method is 846 * initialized with the first occurrence of the section. (This can only happen if {@code addProperty()} has been used in 847 * a way that does not conform to the storage scheme used by {@code INIConfiguration}. If used correctly, there will not 848 * be duplicate sections.)</li> 849 * <li>There is special support for the global section: Passing in <b>null</b> as section name returns a configuration 850 * with the content of the global section (which may also be empty).</li> 851 * </ul> 852 * 853 * @param name the name of the section in question; <b>null</b> represents the global section 854 * @return a configuration containing only the properties of the specified section 855 */ 856 public SubnodeConfiguration getSection(final String name) { 857 if (name == null) { 858 return getGlobalSection(); 859 } 860 try { 861 return (SubnodeConfiguration) configurationAt(name, true); 862 } catch (final ConfigurationRuntimeException iex) { 863 // the passed in key does not map to exactly one node 864 // obtain the node for the section, create it on demand 865 final InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 866 final NodeSelector selector = parentModel.trackChildNodeWithCreation(null, name, this); 867 return createSubConfigurationForTrackedNode(selector, this); 868 } 869 } 870 871 /** 872 * Creates a sub configuration for the global section of the represented INI configuration. 873 * 874 * @return the sub configuration for the global section 875 */ 876 private SubnodeConfiguration getGlobalSection() { 877 final InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 878 final NodeSelector selector = new NodeSelector(null); // selects parent 879 parentModel.trackNode(selector, this); 880 final GlobalSectionNodeModel model = new GlobalSectionNodeModel(this, selector); 881 final SubnodeConfiguration sub = new SubnodeConfiguration(this, model); 882 initSubConfigurationForThisParent(sub); 883 return sub; 884 } 885 886 /** 887 * Checks whether the specified configuration node represents a section. 888 * 889 * @param node the node in question 890 * @return a flag whether this node represents a section 891 */ 892 private static boolean isSectionNode(final ImmutableNode node) { 893 return node.getValue() == null; 894 } 895 896 /** 897 * A specialized node model implementation for the sub configuration representing the global section of the INI file. 898 * This is a regular {@code TrackedNodeModel} with one exception: The {@code NodeHandler} used by this model applies a 899 * filter on the children of the root node so that only nodes are visible that are no sub sections. 900 */ 901 private static final class GlobalSectionNodeModel extends TrackedNodeModel { 902 /** 903 * Creates a new instance of {@code GlobalSectionNodeModel} and initializes it with the given underlying model. 904 * 905 * @param modelSupport the underlying {@code InMemoryNodeModel} 906 * @param selector the {@code NodeSelector} 907 */ 908 public GlobalSectionNodeModel(final InMemoryNodeModelSupport modelSupport, final NodeSelector selector) { 909 super(modelSupport, selector, true); 910 } 911 912 @Override 913 public NodeHandler<ImmutableNode> getNodeHandler() { 914 return new NodeHandlerDecorator<ImmutableNode>() { 915 @Override 916 public List<ImmutableNode> getChildren(final ImmutableNode node) { 917 final List<ImmutableNode> children = super.getChildren(node); 918 return filterChildrenOfGlobalSection(node, children); 919 } 920 921 @Override 922 public List<ImmutableNode> getChildren(final ImmutableNode node, final String name) { 923 final List<ImmutableNode> children = super.getChildren(node, name); 924 return filterChildrenOfGlobalSection(node, children); 925 } 926 927 @Override 928 public int getChildrenCount(final ImmutableNode node, final String name) { 929 final List<ImmutableNode> children = name != null ? super.getChildren(node, name) : super.getChildren(node); 930 return filterChildrenOfGlobalSection(node, children).size(); 931 } 932 933 @Override 934 public ImmutableNode getChild(final ImmutableNode node, final int index) { 935 final List<ImmutableNode> children = super.getChildren(node); 936 return filterChildrenOfGlobalSection(node, children).get(index); 937 } 938 939 @Override 940 public int indexOfChild(final ImmutableNode parent, final ImmutableNode child) { 941 final List<ImmutableNode> children = super.getChildren(parent); 942 return filterChildrenOfGlobalSection(parent, children).indexOf(child); 943 } 944 945 @Override 946 protected NodeHandler<ImmutableNode> getDecoratedNodeHandler() { 947 return GlobalSectionNodeModel.super.getNodeHandler(); 948 } 949 950 /** 951 * Filters the child nodes of the global section. This method checks whether the passed in node is the root node of the 952 * configuration. If so, from the list of children all nodes are filtered which are section nodes. 953 * 954 * @param node the node in question 955 * @param children the children of this node 956 * @return a list with the filtered children 957 */ 958 private List<ImmutableNode> filterChildrenOfGlobalSection(final ImmutableNode node, final List<ImmutableNode> children) { 959 final List<ImmutableNode> filteredList; 960 if (node == getRootNode()) { 961 filteredList = children.stream().filter(child -> !isSectionNode(child)).collect(Collectors.toList()); 962 } else { 963 filteredList = children; 964 } 965 966 return filteredList; 967 } 968 }; 969 } 970 } 971 }