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