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><b>Sections:</b> 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><b>Parameters:</b> Items in a section are known as parameters. Parameters have a typical {@code key = value} 074 * format.</li> 075 * <li><b>Comments:</b> 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><b>Comments:</b> The '#' character is also accepted as a comment signifier.</li> 086 * <li><b>Key value separator:</b> The ':' character is also accepted in place of '=' to separate keys and values in 087 * parameters, for example {@code var1 : foo}.</li> 088 * <li><b>Duplicate sections:</b> 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><b>Duplicate parameters:</b> 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 (e.g. 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 * The flag for decision, whether inline comments on the section line are allowed. 215 */ 216 private boolean sectionInLineCommentsAllowed; 217 218 public INIConfiguration build() { 219 return new INIConfiguration(sectionInLineCommentsAllowed); 220 } 221 222 public Builder setSectionInLineCommentsAllowed(final boolean sectionInLineCommentsAllowed) { 223 this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed; 224 return this; 225 } 226 227 } 228 229 /** 230 * A specialized node model implementation for the sub configuration representing the global section of the INI file. 231 * This is a regular {@code TrackedNodeModel} with one exception: The {@code NodeHandler} used by this model applies a 232 * filter on the children of the root node so that only nodes are visible that are no sub sections. 233 */ 234 private static final class GlobalSectionNodeModel extends TrackedNodeModel { 235 /** 236 * Creates a new instance of {@code GlobalSectionNodeModel} and initializes it with the given underlying model. 237 * 238 * @param modelSupport the underlying {@code InMemoryNodeModel} 239 * @param selector the {@code NodeSelector} 240 */ 241 public GlobalSectionNodeModel(final InMemoryNodeModelSupport modelSupport, final NodeSelector selector) { 242 super(modelSupport, selector, true); 243 } 244 245 @Override 246 public NodeHandler<ImmutableNode> getNodeHandler() { 247 return new NodeHandlerDecorator<ImmutableNode>() { 248 /** 249 * Filters the child nodes of the global section. This method checks whether the passed in node is the root node of the 250 * configuration. If so, from the list of children all nodes are filtered which are section nodes. 251 * 252 * @param node the node in question 253 * @param children the children of this node 254 * @return a list with the filtered children 255 */ 256 private List<ImmutableNode> filterChildrenOfGlobalSection(final ImmutableNode node, final List<ImmutableNode> children) { 257 final List<ImmutableNode> filteredList; 258 if (node == getRootNode()) { 259 filteredList = children.stream().filter(child -> !isSectionNode(child)).collect(Collectors.toList()); 260 } else { 261 filteredList = children; 262 } 263 264 return filteredList; 265 } 266 267 @Override 268 public ImmutableNode getChild(final ImmutableNode node, final int index) { 269 final List<ImmutableNode> children = super.getChildren(node); 270 return filterChildrenOfGlobalSection(node, children).get(index); 271 } 272 273 @Override 274 public List<ImmutableNode> getChildren(final ImmutableNode node) { 275 final List<ImmutableNode> children = super.getChildren(node); 276 return filterChildrenOfGlobalSection(node, children); 277 } 278 279 @Override 280 public List<ImmutableNode> getChildren(final ImmutableNode node, final String name) { 281 final List<ImmutableNode> children = super.getChildren(node, name); 282 return filterChildrenOfGlobalSection(node, children); 283 } 284 285 @Override 286 public int getChildrenCount(final ImmutableNode node, final String name) { 287 final List<ImmutableNode> children = name != null ? super.getChildren(node, name) : super.getChildren(node); 288 return filterChildrenOfGlobalSection(node, children).size(); 289 } 290 291 @Override 292 protected NodeHandler<ImmutableNode> getDecoratedNodeHandler() { 293 return GlobalSectionNodeModel.super.getNodeHandler(); 294 } 295 296 @Override 297 public int indexOfChild(final ImmutableNode parent, final ImmutableNode child) { 298 final List<ImmutableNode> children = super.getChildren(parent); 299 return filterChildrenOfGlobalSection(parent, children).indexOf(child); 300 } 301 }; 302 } 303 } 304 305 /** 306 * The empty key. 307 */ 308 private static final String EMPTY_KEY = " "; 309 310 /** 311 * The default characters that signal the start of a comment line. 312 */ 313 protected static final String COMMENT_CHARS = "#;"; 314 315 /** 316 * The default characters used to separate keys from values. 317 */ 318 protected static final String SEPARATOR_CHARS = "=:"; 319 320 /** 321 * Constant for the line separator. 322 */ 323 private static final String LINE_SEPARATOR = System.lineSeparator(); 324 325 /** 326 * The characters used for quoting values. 327 */ 328 private static final String QUOTE_CHARACTERS = "\"'"; 329 330 /** 331 * The line continuation character. 332 */ 333 private static final String LINE_CONT = "\\"; 334 335 /** 336 * Creates a new builder. 337 * 338 * @return a new builder. 339 * @since 2.9.0 340 */ 341 public static Builder builder() { 342 return new Builder(); 343 } 344 345 /** 346 * Creates a new root node from the builders constructed while reading the configuration file. 347 * 348 * @param rootBuilder the builder for the top-level section 349 * @param sectionBuilders a map storing the section builders 350 * @return the root node of the newly created hierarchy 351 */ 352 private static ImmutableNode createNewRootNode(final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders) { 353 sectionBuilders.forEach((k, v) -> rootBuilder.addChild(v.name(k).create())); 354 return rootBuilder.create(); 355 } 356 357 /** 358 * Checks for the occurrence of the specified separators in the given line. The index of the first separator is 359 * returned. 360 * 361 * @param line the line to be investigated 362 * @param separators a string with the separator characters to look for 363 * @return the lowest index of a separator character or -1 if no separator is found 364 */ 365 private static int findFirstOccurrence(final String line, final String separators) { 366 int index = -1; 367 368 for (int i = 0; i < separators.length(); i++) { 369 final char sep = separators.charAt(i); 370 final int pos = line.indexOf(sep); 371 if (pos >= 0 && (index < 0 || pos < index)) { 372 index = pos; 373 } 374 } 375 376 return index; 377 } 378 379 /** 380 * Searches for a separator character directly before a quoting character. If the first non-whitespace character before 381 * a quote character is a separator, it is considered the "real" separator in this line - even if there are other 382 * separators before. 383 * 384 * @param line the line to be investigated 385 * @param quoteIndex the index of the quote character 386 * @return the index of the separator before the quote or < 0 if there is none 387 */ 388 private static int findSeparatorBeforeQuote(final String line, final int quoteIndex) { 389 int index = quoteIndex - 1; 390 while (index >= 0 && Character.isWhitespace(line.charAt(index))) { 391 index--; 392 } 393 394 if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0) { 395 index = -1; 396 } 397 398 return index; 399 } 400 401 /** 402 * Determine if the given line contains a section - inline comments are allowed. 403 * 404 * @param line The line to check. 405 * @return true if the line contains a section 406 */ 407 private static boolean isNonStrictSection(final String line) { 408 return line.startsWith("[") && line.contains("]"); 409 } 410 411 /** 412 * Checks whether the specified configuration node represents a section. 413 * 414 * @param node the node in question 415 * @return a flag whether this node represents a section 416 */ 417 private static boolean isSectionNode(final ImmutableNode node) { 418 return node.getValue() == null; 419 } 420 421 /** 422 * Determine if the entire given line is a section - inline comments are not allowed. 423 * 424 * @param line The line to check. 425 * @return true if the entire line is a section 426 */ 427 private static boolean isStrictSection(final String line) { 428 return line.startsWith("[") && line.endsWith("]"); 429 } 430 431 /** 432 * Tests whether the specified string contains a line continuation marker. 433 * 434 * @param line the string to check 435 * @return a flag whether this line continues 436 */ 437 private static boolean lineContinues(final String line) { 438 final String s = line.trim(); 439 return s.equals(LINE_CONT) || s.length() > 2 && s.endsWith(LINE_CONT) && Character.isWhitespace(s.charAt(s.length() - 2)); 440 } 441 442 /** 443 * The separator used when writing an INI file. 444 */ 445 private String separatorUsedInOutput = " = "; 446 447 /** 448 * The separator used when reading an INI file. 449 */ 450 private String separatorUsedInInput = SEPARATOR_CHARS; 451 452 /** 453 * The characters used to separate keys from values when reading an INI file. 454 */ 455 private String commentCharsUsedInInput = COMMENT_CHARS; 456 457 /** 458 * The flag for decision, whether inline comments on the section line are allowed. 459 */ 460 private boolean sectionInLineCommentsAllowed; 461 462 /** 463 * Create a new empty INI Configuration. 464 */ 465 public INIConfiguration() { 466 } 467 468 /** 469 * Create a new empty INI Configuration with option to allow inline comments on the section line. 470 * 471 * @param sectionInLineCommentsAllowed when true inline comments on the section line are allowed 472 */ 473 private INIConfiguration(final boolean sectionInLineCommentsAllowed) { 474 this.sectionInLineCommentsAllowed = sectionInLineCommentsAllowed; 475 } 476 477 /** 478 * Creates a new instance of {@code INIConfiguration} with the content of the specified 479 * {@code HierarchicalConfiguration}. 480 * 481 * @param c the configuration to be copied 482 * @since 2.0 483 */ 484 public INIConfiguration(final HierarchicalConfiguration<ImmutableNode> c) { 485 super(c); 486 } 487 488 /** 489 * Reads the content of an INI file from the passed in reader and creates a structure of builders for constructing the 490 * {@code ImmutableNode} objects representing the data. 491 * 492 * @param in the reader 493 * @param rootBuilder the builder for the top-level section 494 * @param sectionBuilders a map storing the section builders 495 * @throws IOException if an I/O error occurs. 496 */ 497 private void createNodeBuilders(final BufferedReader in, final ImmutableNode.Builder rootBuilder, final Map<String, ImmutableNode.Builder> sectionBuilders) 498 throws IOException { 499 ImmutableNode.Builder sectionBuilder = rootBuilder; 500 String line = in.readLine(); 501 while (line != null) { 502 line = line.trim(); 503 if (!isCommentLine(line)) { 504 if (isSectionLine(line)) { 505 final int length = sectionInLineCommentsAllowed ? line.indexOf("]") : line.length() - 1; 506 String section = line.substring(1, length); 507 if (section.isEmpty()) { 508 // use space for sections with no key 509 section = EMPTY_KEY; 510 } 511 sectionBuilder = sectionBuilders.computeIfAbsent(section, k -> new ImmutableNode.Builder()); 512 } else { 513 String key; 514 String value = ""; 515 final int index = findSeparator(line); 516 if (index >= 0) { 517 key = line.substring(0, index); 518 value = parseValue(line.substring(index + 1), in); 519 } else { 520 key = line; 521 } 522 key = key.trim(); 523 if (key.isEmpty()) { 524 // use space for properties with no key 525 key = EMPTY_KEY; 526 } 527 createValueNodes(sectionBuilder, key, value); 528 } 529 } 530 531 line = in.readLine(); 532 } 533 } 534 535 /** 536 * Creates the node(s) for the given key value-pair. If delimiter parsing is enabled, the value string is split if 537 * possible, and for each single value a node is created. Otherwise only a single node is added to the section. 538 * 539 * @param sectionBuilder the section builder for adding new nodes 540 * @param key the key 541 * @param value the value string 542 */ 543 private void createValueNodes(final ImmutableNode.Builder sectionBuilder, final String key, final String value) { 544 getListDelimiterHandler().split(value, false).forEach(v -> sectionBuilder.addChild(new ImmutableNode.Builder().name(key).value(v).create())); 545 } 546 547 /** 548 * Escapes comment characters in the given value. 549 * 550 * @param value the value to be escaped 551 * @return the value with comment characters escaped 552 */ 553 private String escapeComments(final String value) { 554 final String commentChars = getCommentLeadingCharsUsedInInput(); 555 boolean quoted = false; 556 557 for (int i = 0; i < commentChars.length(); i++) { 558 final char c = commentChars.charAt(i); 559 if (value.indexOf(c) != -1) { 560 quoted = true; 561 break; 562 } 563 } 564 565 if (quoted) { 566 return '"' + value.replace("\"", "\\\"") + '"'; 567 } 568 return value; 569 } 570 571 /** 572 * Escapes the given property value before it is written. This method add quotes around the specified value if it 573 * contains a comment character and handles list delimiter characters. 574 * 575 * @param value the string to be escaped 576 */ 577 private String escapeValue(final String value) { 578 return String.valueOf(getListDelimiterHandler().escape(escapeComments(value), ListDelimiterHandler.NOOP_TRANSFORMER)); 579 } 580 581 /** 582 * Tries to find the index of the separator character in the given string. This method checks for the presence of 583 * separator characters in the given string. If multiple characters are found, the first one is assumed to be the 584 * correct separator. If there are quoting characters, they are taken into account, too. 585 * 586 * @param line the line to be checked 587 * @return the index of the separator character or -1 if none is found 588 */ 589 private int findSeparator(final String line) { 590 int index = findSeparatorBeforeQuote(line, findFirstOccurrence(line, QUOTE_CHARACTERS)); 591 if (index < 0) { 592 index = findFirstOccurrence(line, getSeparatorUsedInInput()); 593 } 594 return index; 595 } 596 597 /** 598 * Gets comment leading separator used in INI reading. see {@code setCommentLeadingCharsUsedInInput} for further 599 * explanation 600 * 601 * @return the current separator for reading the INI input 602 * @since 2.5 603 */ 604 public String getCommentLeadingCharsUsedInInput() { 605 beginRead(false); 606 try { 607 return commentCharsUsedInInput; 608 } finally { 609 endRead(); 610 } 611 } 612 613 /** 614 * Creates a sub configuration for the global section of the represented INI configuration. 615 * 616 * @return the sub configuration for the global section 617 */ 618 private SubnodeConfiguration getGlobalSection() { 619 final InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 620 final NodeSelector selector = new NodeSelector(null); // selects parent 621 parentModel.trackNode(selector, this); 622 final GlobalSectionNodeModel model = new GlobalSectionNodeModel(this, selector); 623 final SubnodeConfiguration sub = new SubnodeConfiguration(this, model); 624 initSubConfigurationForThisParent(sub); 625 return sub; 626 } 627 628 /** 629 * Gets a configuration with the content of the specified section. This provides an easy way of working with a single 630 * section only. The way this configuration is structured internally, this method is very similar to calling 631 * {@link HierarchicalConfiguration#configurationAt(String)} with the name of the section in question. There are the 632 * following differences however: 633 * <ul> 634 * <li>This method never throws an exception. If the section does not exist, it is created now. The configuration 635 * returned in this case is empty.</li> 636 * <li>If section is contained multiple times in the configuration, the configuration returned by this method is 637 * initialized with the first occurrence of the section. (This can only happen if {@code addProperty()} has been used in 638 * a way that does not conform to the storage scheme used by {@code INIConfiguration}. If used correctly, there will not 639 * be duplicate sections.)</li> 640 * <li>There is special support for the global section: Passing in <b>null</b> as section name returns a configuration 641 * with the content of the global section (which may also be empty).</li> 642 * </ul> 643 * 644 * @param name the name of the section in question; <b>null</b> represents the global section 645 * @return a configuration containing only the properties of the specified section 646 */ 647 public SubnodeConfiguration getSection(final String name) { 648 if (name == null) { 649 return getGlobalSection(); 650 } 651 try { 652 return (SubnodeConfiguration) configurationAt(name, true); 653 } catch (final ConfigurationRuntimeException iex) { 654 // the passed in key does not map to exactly one node 655 // obtain the node for the section, create it on demand 656 final InMemoryNodeModel parentModel = getSubConfigurationParentModel(); 657 final NodeSelector selector = parentModel.trackChildNodeWithCreation(null, name, this); 658 return createSubConfigurationForTrackedNode(selector, this); 659 } 660 } 661 662 /** 663 * Gets a set containing the sections in this ini configuration. Note that changes to this set do not affect the 664 * configuration. 665 * 666 * @return a set containing the sections. 667 */ 668 public Set<String> getSections() { 669 final Set<String> sections = new LinkedHashSet<>(); 670 boolean globalSection = false; 671 boolean inSection = false; 672 673 beginRead(false); 674 try { 675 for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) { 676 if (isSectionNode(node)) { 677 inSection = true; 678 sections.add(node.getNodeName()); 679 } else if (!inSection && !globalSection) { 680 globalSection = true; 681 sections.add(null); 682 } 683 } 684 } finally { 685 endRead(); 686 } 687 688 return sections; 689 } 690 691 /** 692 * Gets separator used in INI reading. see {@code setSeparatorUsedInInput} for further explanation 693 * 694 * @return the current separator for reading the INI input 695 * @since 2.5 696 */ 697 public String getSeparatorUsedInInput() { 698 beginRead(false); 699 try { 700 return separatorUsedInInput; 701 } finally { 702 endRead(); 703 } 704 } 705 706 /** 707 * Gets separator used in INI output. see {@code setSeparatorUsedInOutput} for further explanation 708 * 709 * @return the current separator for writing the INI output 710 * @since 2.2 711 */ 712 public String getSeparatorUsedInOutput() { 713 beginRead(false); 714 try { 715 return separatorUsedInOutput; 716 } finally { 717 endRead(); 718 } 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 * Determine 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 * Determine 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 * Parse 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, e.g.: 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 * Load 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 * Allows setting 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 * Allows setting 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 * Allows setting 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 beginWrite(false); 927 try { 928 this.separatorUsedInOutput = separator; 929 } finally { 930 endWrite(); 931 } 932 } 933 934 /** 935 * Save the configuration to the specified writer. 936 * 937 * @param writer - The writer to save the configuration to. 938 * @throws ConfigurationException If an error occurs while writing the configuration 939 * @throws IOException if an I/O error occurs. 940 */ 941 @Override 942 public void write(final Writer writer) throws ConfigurationException, IOException { 943 final PrintWriter out = new PrintWriter(writer); 944 boolean first = true; 945 final String separator = getSeparatorUsedInOutput(); 946 947 beginRead(false); 948 try { 949 for (final ImmutableNode node : getModel().getNodeHandler().getRootNode().getChildren()) { 950 if (isSectionNode(node)) { 951 if (!first) { 952 out.println(); 953 } 954 out.print("["); 955 out.print(node.getNodeName()); 956 out.print("]"); 957 out.println(); 958 959 node.forEach(child -> writeProperty(out, child.getNodeName(), child.getValue(), separator)); 960 } else { 961 writeProperty(out, node.getNodeName(), node.getValue(), separator); 962 } 963 first = false; 964 } 965 out.println(); 966 out.flush(); 967 } finally { 968 endRead(); 969 } 970 } 971 972 /** 973 * Writes data about a property into the given stream. 974 * 975 * @param out the output stream 976 * @param key the key 977 * @param value the value 978 */ 979 private void writeProperty(final PrintWriter out, final String key, final Object value, final String separator) { 980 out.print(key); 981 out.print(separator); 982 out.print(escapeValue(value.toString())); 983 out.println(); 984 } 985}