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