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