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