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