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="https://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 * @since 1.6 221 */ 222public class INIConfiguration extends BaseHierarchicalConfiguration implements 223 FileBasedConfiguration 224{ 225 /** 226 * The default characters that signal the start of a comment line. 227 */ 228 protected static final String COMMENT_CHARS = "#;"; 229 230 /** 231 * The default characters used to separate keys from values. 232 */ 233 protected static final String SEPARATOR_CHARS = "=:"; 234 235 /** 236 * Constant for the line separator. 237 */ 238 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 239 240 /** 241 * The characters used for quoting values. 242 */ 243 private static final String QUOTE_CHARACTERS = "\"'"; 244 245 /** 246 * The line continuation character. 247 */ 248 private static final String LINE_CONT = "\\"; 249 250 /** 251 * The separator used when writing an INI file. 252 */ 253 private String separatorUsedInOutput = " = "; 254 255 /** 256 * The separator used when reading an INI file. 257 */ 258 private String separatorUsedInInput = SEPARATOR_CHARS; 259 260 /** 261 * The characters used to separate keys from values 262 * when reading an INI file. 263 */ 264 private String commentCharsUsedInInput = COMMENT_CHARS; 265 266 /** 267 * Create a new empty INI Configuration. 268 */ 269 public INIConfiguration() 270 { 271 super(); 272 } 273 274 /** 275 * Creates a new instance of {@code INIConfiguration} with the 276 * content of the specified {@code HierarchicalConfiguration}. 277 * 278 * @param c the configuration to be copied 279 * @since 2.0 280 */ 281 public INIConfiguration(final HierarchicalConfiguration<ImmutableNode> c) 282 { 283 super(c); 284 } 285 286 /** 287 * Get separator used in INI output. see {@code setSeparatorUsedInOutput} 288 * for further explanation 289 * 290 * @return the current separator for writing the INI output 291 * @since 2.2 292 */ 293 public String getSeparatorUsedInOutput() 294 { 295 beginRead(false); 296 try 297 { 298 return separatorUsedInOutput; 299 } 300 finally 301 { 302 endRead(); 303 } 304 } 305 306 /** 307 * Allows setting the key and value separator which is used for the creation 308 * of the resulting INI output 309 * 310 * @param separator String of the new separator for INI output 311 * @since 2.2 312 */ 313 public void setSeparatorUsedInOutput(final String separator) 314 { 315 beginWrite(false); 316 try 317 { 318 this.separatorUsedInOutput = separator; 319 } 320 finally 321 { 322 endWrite(); 323 } 324 } 325 326 /** 327 * Get separator used in INI reading. see {@code setSeparatorUsedInInput} 328 * for further explanation 329 * 330 * @return the current separator for reading the INI input 331 * @since 2.5 332 */ 333 public String getSeparatorUsedInInput() 334 { 335 beginRead(false); 336 try 337 { 338 return separatorUsedInInput; 339 } 340 finally 341 { 342 endRead(); 343 } 344 } 345 346 /** 347 * Allows setting the key and value separator which is used in reading 348 * an INI file 349 * 350 * @param separator String of the new separator for INI reading 351 * @since 2.5 352 */ 353 public void setSeparatorUsedInInput(final String separator) 354 { 355 beginRead(false); 356 try 357 { 358 this.separatorUsedInInput = separator; 359 } 360 finally 361 { 362 endRead(); 363 } 364 } 365 366 /** 367 * Get comment leading separator used in INI reading. 368 * see {@code setCommentLeadingCharsUsedInInput} for further explanation 369 * 370 * @return the current separator for reading the INI input 371 * @since 2.5 372 */ 373 public String getCommentLeadingCharsUsedInInput() 374 { 375 beginRead(false); 376 try 377 { 378 return commentCharsUsedInInput; 379 } 380 finally 381 { 382 endRead(); 383 } 384 } 385 386 /** 387 * Allows setting the leading comment separator which is used in reading 388 * an INI file 389 * 390 * @param separator String of the new separator for INI reading 391 * @since 2.5 392 */ 393 public void setCommentLeadingCharsUsedInInput(final String separator) 394 { 395 beginRead(false); 396 try 397 { 398 this.commentCharsUsedInInput = separator; 399 } 400 finally 401 { 402 endRead(); 403 } 404 } 405 406 /** 407 * Save the configuration to the specified writer. 408 * 409 * @param writer - The writer to save the configuration to. 410 * @throws ConfigurationException If an error occurs while writing the 411 * configuration 412 * @throws IOException if an I/O error occurs 413 */ 414 @Override 415 public void write(final Writer writer) throws ConfigurationException, IOException 416 { 417 final PrintWriter out = new PrintWriter(writer); 418 boolean first = true; 419 final String separator = getSeparatorUsedInOutput(); 420 421 beginRead(false); 422 try 423 { 424 for (final ImmutableNode node : getModel().getNodeHandler().getRootNode() 425 .getChildren()) 426 { 427 if (isSectionNode(node)) 428 { 429 if (!first) 430 { 431 out.println(); 432 } 433 out.print("["); 434 out.print(node.getNodeName()); 435 out.print("]"); 436 out.println(); 437 438 for (final ImmutableNode child : node.getChildren()) 439 { 440 writeProperty(out, child.getNodeName(), 441 child.getValue(), separator); 442 } 443 } 444 else 445 { 446 writeProperty(out, node.getNodeName(), node.getValue(), separator); 447 } 448 first = false; 449 } 450 out.println(); 451 out.flush(); 452 } 453 finally 454 { 455 endRead(); 456 } 457 } 458 459 /** 460 * Load the configuration from the given reader. Note that the 461 * {@code clear()} method is not called so the configuration read in will 462 * be merged with the current configuration. 463 * 464 * @param in the reader to read the configuration from. 465 * @throws ConfigurationException If an error occurs while reading the 466 * configuration 467 * @throws IOException if an I/O error occurs 468 */ 469 @Override 470 public void read(final Reader in) throws ConfigurationException, IOException 471 { 472 final BufferedReader bufferedReader = new BufferedReader(in); 473 final Map<String, ImmutableNode.Builder> sectionBuilders = new LinkedHashMap<>(); 474 final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(); 475 476 createNodeBuilders(bufferedReader, rootBuilder, sectionBuilders); 477 final ImmutableNode rootNode = createNewRootNode(rootBuilder, sectionBuilders); 478 addNodes(null, rootNode.getChildren()); 479 } 480 481 /** 482 * Creates a new root node from the builders constructed while reading the 483 * configuration file. 484 * 485 * @param rootBuilder the builder for the top-level section 486 * @param sectionBuilders a map storing the section builders 487 * @return the root node of the newly created hierarchy 488 */ 489 private static ImmutableNode createNewRootNode( 490 final ImmutableNode.Builder rootBuilder, 491 final Map<String, ImmutableNode.Builder> sectionBuilders) 492 { 493 for (final Map.Entry<String, ImmutableNode.Builder> e : sectionBuilders 494 .entrySet()) 495 { 496 rootBuilder.addChild(e.getValue().name(e.getKey()).create()); 497 } 498 return rootBuilder.create(); 499 } 500 501 /** 502 * Reads the content of an INI file from the passed in reader and creates a 503 * structure of builders for constructing the {@code ImmutableNode} objects 504 * representing the data. 505 * 506 * @param in the reader 507 * @param rootBuilder the builder for the top-level section 508 * @param sectionBuilders a map storing the section builders 509 * @throws IOException if an I/O error occurs 510 */ 511 private void createNodeBuilders(final BufferedReader in, 512 final ImmutableNode.Builder rootBuilder, 513 final Map<String, ImmutableNode.Builder> sectionBuilders) 514 throws IOException 515 { 516 ImmutableNode.Builder sectionBuilder = rootBuilder; 517 String line = in.readLine(); 518 while (line != null) 519 { 520 line = line.trim(); 521 if (!isCommentLine(line)) 522 { 523 if (isSectionLine(line)) 524 { 525 final String section = line.substring(1, line.length() - 1); 526 sectionBuilder = sectionBuilders.get(section); 527 if (sectionBuilder == null) 528 { 529 sectionBuilder = new ImmutableNode.Builder(); 530 sectionBuilders.put(section, sectionBuilder); 531 } 532 } 533 534 else 535 { 536 String key; 537 String value = ""; 538 final int index = findSeparator(line); 539 if (index >= 0) 540 { 541 key = line.substring(0, index); 542 value = parseValue(line.substring(index + 1), in); 543 } 544 else 545 { 546 key = line; 547 } 548 key = key.trim(); 549 if (key.length() < 1) 550 { 551 // use space for properties with no key 552 key = " "; 553 } 554 createValueNodes(sectionBuilder, key, value); 555 } 556 } 557 558 line = in.readLine(); 559 } 560 } 561 562 /** 563 * Creates the node(s) for the given key value-pair. If delimiter parsing is 564 * enabled, the value string is split if possible, and for each single value 565 * a node is created. Otherwise only a single node is added to the section. 566 * 567 * @param sectionBuilder the section builder for adding new nodes 568 * @param key the key 569 * @param value the value string 570 */ 571 private void createValueNodes(final ImmutableNode.Builder sectionBuilder, 572 final String key, final String value) 573 { 574 final Collection<String> values = 575 getListDelimiterHandler().split(value, false); 576 577 for (final String v : values) 578 { 579 sectionBuilder.addChild(new ImmutableNode.Builder().name(key) 580 .value(v).create()); 581 } 582 } 583 584 /** 585 * Writes data about a property into the given stream. 586 * 587 * @param out the output stream 588 * @param key the key 589 * @param value the value 590 */ 591 private void writeProperty(final PrintWriter out, final String key, final Object value, final String separator) 592 { 593 out.print(key); 594 out.print(separator); 595 out.print(escapeValue(value.toString())); 596 out.println(); 597 } 598 599 /** 600 * Parse the value to remove the quotes and ignoring the comment. Example: 601 * 602 * <pre> 603 * "value" ; comment -> value 604 * </pre> 605 * 606 * <pre> 607 * 'value' ; comment -> value 608 * </pre> 609 * Note that a comment character is only recognized if there is at least one 610 * whitespace character before it. So it can appear in the property value, 611 * e.g.: 612 * <pre> 613 * C:\\Windows;C:\\Windows\\system32 614 * </pre> 615 * 616 * @param val the value to be parsed 617 * @param reader the reader (needed if multiple lines have to be read) 618 * @throws IOException if an IO error occurs 619 */ 620 private String parseValue(final String val, final BufferedReader reader) 621 throws IOException 622 { 623 final StringBuilder propertyValue = new StringBuilder(); 624 boolean lineContinues; 625 String value = val.trim(); 626 627 do 628 { 629 final boolean quoted = value.startsWith("\"") || value.startsWith("'"); 630 boolean stop = false; 631 boolean escape = false; 632 633 final char quote = quoted ? value.charAt(0) : 0; 634 635 int i = quoted ? 1 : 0; 636 637 final StringBuilder result = new StringBuilder(); 638 char lastChar = 0; 639 while (i < value.length() && !stop) 640 { 641 final char c = value.charAt(i); 642 643 if (quoted) 644 { 645 if ('\\' == c && !escape) 646 { 647 escape = true; 648 } 649 else if (!escape && quote == c) 650 { 651 stop = true; 652 } 653 else if (escape && quote == c) 654 { 655 escape = false; 656 result.append(c); 657 } 658 else 659 { 660 if (escape) 661 { 662 escape = false; 663 result.append('\\'); 664 } 665 666 result.append(c); 667 } 668 } 669 else 670 { 671 if (isCommentChar(c) && Character.isWhitespace(lastChar)) 672 { 673 stop = true; 674 } 675 else 676 { 677 result.append(c); 678 } 679 } 680 681 i++; 682 lastChar = c; 683 } 684 685 String v = result.toString(); 686 if (!quoted) 687 { 688 v = v.trim(); 689 lineContinues = lineContinues(v); 690 if (lineContinues) 691 { 692 // remove trailing "\" 693 v = v.substring(0, v.length() - 1).trim(); 694 } 695 } 696 else 697 { 698 lineContinues = lineContinues(value, i); 699 } 700 propertyValue.append(v); 701 702 if (lineContinues) 703 { 704 propertyValue.append(LINE_SEPARATOR); 705 value = reader.readLine(); 706 } 707 } while (lineContinues && value != null); 708 709 return propertyValue.toString(); 710 } 711 712 /** 713 * Tests whether the specified string contains a line continuation marker. 714 * 715 * @param line the string to check 716 * @return a flag whether this line continues 717 */ 718 private static boolean lineContinues(final String line) 719 { 720 final String s = line.trim(); 721 return s.equals(LINE_CONT) 722 || (s.length() > 2 && s.endsWith(LINE_CONT) && Character 723 .isWhitespace(s.charAt(s.length() - 2))); 724 } 725 726 /** 727 * Tests whether the specified string contains a line continuation marker 728 * after the specified position. This method parses the string to remove a 729 * comment that might be present. Then it checks whether a line continuation 730 * marker can be found at the end. 731 * 732 * @param line the line to check 733 * @param pos the start position 734 * @return a flag whether this line continues 735 */ 736 private boolean lineContinues(final String line, final int pos) 737 { 738 String s; 739 740 if (pos >= line.length()) 741 { 742 s = line; 743 } 744 else 745 { 746 int end = pos; 747 while (end < line.length() && !isCommentChar(line.charAt(end))) 748 { 749 end++; 750 } 751 s = line.substring(pos, end); 752 } 753 754 return lineContinues(s); 755 } 756 757 /** 758 * Tests whether the specified character is a comment character. 759 * 760 * @param c the character 761 * @return a flag whether this character starts a comment 762 */ 763 private boolean isCommentChar(final char c) 764 { 765 return getCommentLeadingCharsUsedInInput().indexOf(c) >= 0; 766 } 767 768 /** 769 * Tries to find the index of the separator character in the given string. 770 * This method checks for the presence of separator characters in the given 771 * string. If multiple characters are found, the first one is assumed to be 772 * the correct separator. If there are quoting characters, they are taken 773 * into account, too. 774 * 775 * @param line the line to be checked 776 * @return the index of the separator character or -1 if none is found 777 */ 778 private int findSeparator(final String line) 779 { 780 int index = 781 findSeparatorBeforeQuote(line, 782 findFirstOccurrence(line, QUOTE_CHARACTERS)); 783 if (index < 0) 784 { 785 index = findFirstOccurrence(line, getSeparatorUsedInInput()); 786 } 787 return index; 788 } 789 790 /** 791 * Checks for the occurrence of the specified separators in the given line. 792 * The index of the first separator is returned. 793 * 794 * @param line the line to be investigated 795 * @param separators a string with the separator characters to look for 796 * @return the lowest index of a separator character or -1 if no separator 797 * is found 798 */ 799 private static int findFirstOccurrence(final String line, final String separators) 800 { 801 int index = -1; 802 803 for (int i = 0; i < separators.length(); i++) 804 { 805 final char sep = separators.charAt(i); 806 final int pos = line.indexOf(sep); 807 if (pos >= 0) 808 { 809 if (index < 0 || pos < index) 810 { 811 index = pos; 812 } 813 } 814 } 815 816 return index; 817 } 818 819 /** 820 * Searches for a separator character directly before a quoting character. 821 * If the first non-whitespace character before a quote character is a 822 * separator, it is considered the "real" separator in this line - even if 823 * there are other separators before. 824 * 825 * @param line the line to be investigated 826 * @param quoteIndex the index of the quote character 827 * @return the index of the separator before the quote or < 0 if there is 828 * none 829 */ 830 private static int findSeparatorBeforeQuote(final String line, final int quoteIndex) 831 { 832 int index = quoteIndex - 1; 833 while (index >= 0 && Character.isWhitespace(line.charAt(index))) 834 { 835 index--; 836 } 837 838 if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0) 839 { 840 index = -1; 841 } 842 843 return index; 844 } 845 846 /** 847 * Escapes the given property value before it is written. This method add 848 * quotes around the specified value if it contains a comment character and 849 * handles list delimiter characters. 850 * 851 * @param value the string to be escaped 852 */ 853 private String escapeValue(final String value) 854 { 855 return String.valueOf(getListDelimiterHandler().escape( 856 escapeComments(value), ListDelimiterHandler.NOOP_TRANSFORMER)); 857 } 858 859 /** 860 * Escapes comment characters in the given value. 861 * 862 * @param value the value to be escaped 863 * @return the value with comment characters escaped 864 */ 865 private String escapeComments(final String value) 866 { 867 final String commentChars = getCommentLeadingCharsUsedInInput(); 868 boolean quoted = false; 869 870 for (int i = 0; i < commentChars.length() && !quoted; i++) 871 { 872 final char c = commentChars.charAt(i); 873 if (value.indexOf(c) != -1) 874 { 875 quoted = true; 876 } 877 } 878 879 if (quoted) 880 { 881 return '"' + value.replaceAll("\"", "\\\\\\\"") + '"'; 882 } 883 return value; 884 } 885 886 /** 887 * Determine if the given line is a comment line. 888 * 889 * @param line The line to check. 890 * @return true if the line is empty or starts with one of the comment 891 * characters 892 */ 893 protected boolean isCommentLine(final String line) 894 { 895 if (line == null) 896 { 897 return false; 898 } 899 // blank lines are also treated as comment lines 900 return line.length() < 1 901 || getCommentLeadingCharsUsedInInput().indexOf(line.charAt(0)) >= 0; 902 } 903 904 /** 905 * Determine if the given line is a section. 906 * 907 * @param line The line to check. 908 * @return true if the line contains a section 909 */ 910 protected boolean isSectionLine(final String line) 911 { 912 if (line == null) 913 { 914 return false; 915 } 916 return line.startsWith("[") && line.endsWith("]"); 917 } 918 919 /** 920 * Return a set containing the sections in this ini configuration. Note that 921 * changes to this set do not affect the configuration. 922 * 923 * @return a set containing the sections. 924 */ 925 public Set<String> getSections() 926 { 927 final Set<String> sections = new LinkedHashSet<>(); 928 boolean globalSection = false; 929 boolean inSection = false; 930 931 beginRead(false); 932 try 933 { 934 for (final ImmutableNode node : getModel().getNodeHandler().getRootNode() 935 .getChildren()) 936 { 937 if (isSectionNode(node)) 938 { 939 inSection = true; 940 sections.add(node.getNodeName()); 941 } 942 else 943 { 944 if (!inSection && !globalSection) 945 { 946 globalSection = true; 947 sections.add(null); 948 } 949 } 950 } 951 } 952 finally 953 { 954 endRead(); 955 } 956 957 return sections; 958 } 959 960 /** 961 * Returns a configuration with the content of the specified section. This 962 * provides an easy way of working with a single section only. The way this 963 * configuration is structured internally, this method is very similar to 964 * calling {@link HierarchicalConfiguration#configurationAt(String)} with 965 * the name of the section in question. There are the following differences 966 * however: 967 * <ul> 968 * <li>This method never throws an exception. If the section does not exist, 969 * it is created now. The configuration returned in this case is empty.</li> 970 * <li>If section is contained multiple times in the configuration, the 971 * configuration returned by this method is initialized with the first 972 * occurrence of the section. (This can only happen if 973 * {@code addProperty()} has been used in a way that does not conform 974 * to the storage scheme used by {@code INIConfiguration}. 975 * If used correctly, there will not be duplicate sections.)</li> 976 * <li>There is special support for the global section: Passing in 977 * <b>null</b> as section name returns a configuration with the content of 978 * the global section (which may also be empty).</li> 979 * </ul> 980 * 981 * @param name the name of the section in question; <b>null</b> represents 982 * the global section 983 * @return a configuration containing only the properties of the specified 984 * section 985 */ 986 public SubnodeConfiguration getSection(final String name) 987 { 988 if (name == null) 989 { 990 return getGlobalSection(); 991 } 992 try 993 { 994 return (SubnodeConfiguration) configurationAt(name, true); 995 } 996 catch (final ConfigurationRuntimeException iex) 997 { 998 // the passed in key does not map to exactly one node 999 // obtain the node for the section, create it on demand 1000 final InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 1001 final NodeSelector selector = parentModel.trackChildNodeWithCreation(null, name, this); 1002 return createSubConfigurationForTrackedNode(selector, this); 1003 } 1004 } 1005 1006 /** 1007 * Creates a sub configuration for the global section of the represented INI 1008 * configuration. 1009 * 1010 * @return the sub configuration for the global section 1011 */ 1012 private SubnodeConfiguration getGlobalSection() 1013 { 1014 final InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 1015 final NodeSelector selector = new NodeSelector(null); // selects parent 1016 parentModel.trackNode(selector, this); 1017 final GlobalSectionNodeModel model = 1018 new GlobalSectionNodeModel(this, selector); 1019 final SubnodeConfiguration sub = new SubnodeConfiguration(this, model); 1020 initSubConfigurationForThisParent(sub); 1021 return sub; 1022 } 1023 1024 /** 1025 * Checks whether the specified configuration node represents a section. 1026 * 1027 * @param node the node in question 1028 * @return a flag whether this node represents a section 1029 */ 1030 private static boolean isSectionNode(final ImmutableNode node) 1031 { 1032 return node.getValue() == null; 1033 } 1034 1035 /** 1036 * A specialized node model implementation for the sub configuration 1037 * representing the global section of the INI file. This is a regular 1038 * {@code TrackedNodeModel} with one exception: The {@code NodeHandler} used 1039 * by this model applies a filter on the children of the root node so that 1040 * only nodes are visible that are no sub sections. 1041 */ 1042 private static class GlobalSectionNodeModel extends TrackedNodeModel 1043 { 1044 /** 1045 * Creates a new instance of {@code GlobalSectionNodeModel} and 1046 * initializes it with the given underlying model. 1047 * 1048 * @param modelSupport the underlying {@code InMemoryNodeModel} 1049 * @param selector the {@code NodeSelector} 1050 */ 1051 public GlobalSectionNodeModel(final InMemoryNodeModelSupport modelSupport, 1052 final NodeSelector selector) 1053 { 1054 super(modelSupport, selector, true); 1055 } 1056 1057 @Override 1058 public NodeHandler<ImmutableNode> getNodeHandler() 1059 { 1060 return new NodeHandlerDecorator<ImmutableNode>() 1061 { 1062 @Override 1063 public List<ImmutableNode> getChildren(final ImmutableNode node) 1064 { 1065 final List<ImmutableNode> children = super.getChildren(node); 1066 return filterChildrenOfGlobalSection(node, children); 1067 } 1068 1069 @Override 1070 public List<ImmutableNode> getChildren(final ImmutableNode node, 1071 final String name) 1072 { 1073 final List<ImmutableNode> children = 1074 super.getChildren(node, name); 1075 return filterChildrenOfGlobalSection(node, children); 1076 } 1077 1078 @Override 1079 public int getChildrenCount(final ImmutableNode node, final String name) 1080 { 1081 final List<ImmutableNode> children = 1082 (name != null) ? super.getChildren(node, name) 1083 : super.getChildren(node); 1084 return filterChildrenOfGlobalSection(node, children).size(); 1085 } 1086 1087 @Override 1088 public ImmutableNode getChild(final ImmutableNode node, final int index) 1089 { 1090 final List<ImmutableNode> children = super.getChildren(node); 1091 return filterChildrenOfGlobalSection(node, children).get( 1092 index); 1093 } 1094 1095 @Override 1096 public int indexOfChild(final ImmutableNode parent, 1097 final ImmutableNode child) 1098 { 1099 final List<ImmutableNode> children = super.getChildren(parent); 1100 return filterChildrenOfGlobalSection(parent, children) 1101 .indexOf(child); 1102 } 1103 1104 @Override 1105 protected NodeHandler<ImmutableNode> getDecoratedNodeHandler() 1106 { 1107 return GlobalSectionNodeModel.super.getNodeHandler(); 1108 } 1109 1110 /** 1111 * Filters the child nodes of the global section. This method 1112 * checks whether the passed in node is the root node of the 1113 * configuration. If so, from the list of children all nodes are 1114 * filtered which are section nodes. 1115 * 1116 * @param node the node in question 1117 * @param children the children of this node 1118 * @return a list with the filtered children 1119 */ 1120 private List<ImmutableNode> filterChildrenOfGlobalSection( 1121 final ImmutableNode node, final List<ImmutableNode> children) 1122 { 1123 List<ImmutableNode> filteredList; 1124 if (node == getRootNode()) 1125 { 1126 filteredList = 1127 new ArrayList<>(children.size()); 1128 for (final ImmutableNode child : children) 1129 { 1130 if (!isSectionNode(child)) 1131 { 1132 filteredList.add(child); 1133 } 1134 } 1135 } 1136 else 1137 { 1138 filteredList = children; 1139 } 1140 1141 return filteredList; 1142 } 1143 }; 1144 } 1145 } 1146}