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
18 package org.apache.commons.configuration2;
19
20 import java.io.FileNotFoundException;
21 import java.io.FilterWriter;
22 import java.io.IOException;
23 import java.io.LineNumberReader;
24 import java.io.Reader;
25 import java.io.Writer;
26 import java.net.URL;
27 import java.nio.charset.StandardCharsets;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.Deque;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 import org.apache.commons.configuration2.convert.ListDelimiterHandler;
39 import org.apache.commons.configuration2.convert.ValueTransformer;
40 import org.apache.commons.configuration2.event.ConfigurationEvent;
41 import org.apache.commons.configuration2.ex.ConfigurationException;
42 import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
43 import org.apache.commons.configuration2.io.FileHandler;
44 import org.apache.commons.configuration2.io.FileLocator;
45 import org.apache.commons.configuration2.io.FileLocatorAware;
46 import org.apache.commons.configuration2.io.FileLocatorUtils;
47 import org.apache.commons.lang3.ArrayUtils;
48 import org.apache.commons.lang3.StringUtils;
49 import org.apache.commons.text.StringEscapeUtils;
50 import org.apache.commons.text.translate.AggregateTranslator;
51 import org.apache.commons.text.translate.CharSequenceTranslator;
52 import org.apache.commons.text.translate.EntityArrays;
53 import org.apache.commons.text.translate.LookupTranslator;
54 import org.apache.commons.text.translate.UnicodeEscaper;
55
56 /**
57 * This is the "classic" Properties loader which loads the values from a single or multiple files (which can be chained
58 * with "include =". All given path references are either absolute or relative to the file name supplied in the
59 * constructor.
60 * <p>
61 * In this class, empty PropertyConfigurations can be built, properties added and later saved. include statements are
62 * (obviously) not supported if you don't construct a PropertyConfiguration from a file.
63 *
64 * <p>
65 * The properties file syntax is explained here, basically it follows the syntax of the stream parsed by
66 * {@link java.util.Properties#load} and adds several useful extensions:
67 *
68 * <ul>
69 * <li>Each property has the syntax {@code key <separator> value}. The separators accepted are {@code '='},
70 * {@code ':'} and any white space character. Examples:
71 *
72 * <pre>
73 * key1 = value1
74 * key2 : value2
75 * key3 value3
76 * </pre>
77 *
78 * </li>
79 * <li>The <em>key</em> may use any character, separators must be escaped:
80 *
81 * <pre>
82 * key\:foo = bar
83 * </pre>
84 *
85 * </li>
86 * <li><em>value</em> may be separated on different lines if a backslash is placed at the end of the line that continues
87 * below.</li>
88 * <li>The list delimiter facilities provided by {@link AbstractConfiguration} are supported, too. If an appropriate
89 * {@link ListDelimiterHandler} is set (for instance a
90 * {@link org.apache.commons.configuration2.convert.DefaultListDelimiterHandler D efaultListDelimiterHandler} object
91 * configured with a comma as delimiter character), <em>value</em> can contain <em>value delimiters</em> and will then be
92 * interpreted as a list of tokens. So the following property definition
93 *
94 * <pre>
95 * key = This property, has multiple, values
96 * </pre>
97 *
98 * will result in a property with three values. You can change the handling of delimiters using the
99 * {@link AbstractConfiguration#setListDelimiterHandler(ListDelimiterHandler)} method. Per default, list splitting is
100 * disabled.</li>
101 * <li>Commas in each token are escaped placing a backslash right before the comma.</li>
102 * <li>If a <em>key</em> is used more than once, the values are appended like if they were on the same line separated with
103 * commas. <em>Note</em>: When the configuration file is written back to disk the associated
104 * {@link PropertiesConfigurationLayout} object (see below) will try to preserve as much of the original format as
105 * possible, i.e. properties with multiple values defined on a single line will also be written back on a single line,
106 * and multiple occurrences of a single key will be written on multiple lines. If the {@code addProperty()} method was
107 * called multiple times for adding multiple values to a property, these properties will per default be written on
108 * multiple lines in the output file, too. Some options of the {@code PropertiesConfigurationLayout} class have
109 * influence on that behavior.</li>
110 * <li>Blank lines and lines starting with character '#' or '!' are skipped.</li>
111 * <li>If a property is named "include" (or whatever is defined by setInclude() and getInclude() and the value of that
112 * property is the full path to a file on disk, that file will be included into the configuration. You can also pull in
113 * files relative to the parent configuration file. So if you have something like the following:
114 *
115 * include = additional.properties
116 *
117 * Then "additional.properties" is expected to be in the same directory as the parent configuration file.
118 *
119 * The properties in the included file are added to the parent configuration, they do not replace existing properties
120 * with the same key.
121 *
122 * </li>
123 * <li>You can define custom error handling for the special key {@code "include"} by using
124 * {@link #setIncludeListener(ConfigurationConsumer)}.</li>
125 * </ul>
126 *
127 * <p>
128 * Here is an example of a valid extended properties file:
129 * </p>
130 *
131 * <pre>
132 * # lines starting with # are comments
133 *
134 * # This is the simplest property
135 * key = value
136 *
137 * # A long property may be separated on multiple lines
138 * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
139 * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
140 *
141 * # This is a property with many tokens
142 * tokens_on_a_line = first token, second token
143 *
144 * # This sequence generates exactly the same result
145 * tokens_on_multiple_lines = first token
146 * tokens_on_multiple_lines = second token
147 *
148 * # commas may be escaped in tokens
149 * commas.escaped = Hi\, what'up?
150 *
151 * # properties can reference other properties
152 * base.prop = /base
153 * first.prop = ${base.prop}/first
154 * second.prop = ${first.prop}/second
155 * </pre>
156 *
157 * <p>
158 * A {@code PropertiesConfiguration} object is associated with an instance of the {@link PropertiesConfigurationLayout}
159 * class, which is responsible for storing the layout of the parsed properties file (i.e. empty lines, comments, and
160 * such things). The {@code getLayout()} method can be used to obtain this layout object. With {@code setLayout()} a new
161 * layout object can be set. This should be done before a properties file was loaded.
162 * <p>
163 * Like other {@code Configuration} implementations, this class uses a {@code Synchronizer} object to control concurrent
164 * access. By choosing a suitable implementation of the {@code Synchronizer} interface, an instance can be made
165 * thread-safe or not. Note that access to most of the properties typically set through a builder is not protected by
166 * the {@code Synchronizer}. The intended usage is that these properties are set once at construction time through the
167 * builder and after that remain constant. If you wish to change such properties during life time of an instance, you
168 * have to use the {@code lock()} and {@code unlock()} methods manually to ensure that other threads see your changes.
169 * <p>
170 * As this class extends {@link AbstractConfiguration}, all basic features like variable interpolation, list handling,
171 * or data type conversions are available as well. This is described in the chapter
172 * <a href="https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html"> Basic features
173 * and AbstractConfiguration</a> of the user's guide. There is also a separate chapter dealing with
174 * <a href="commons.apache.org/proper/commons-configuration/userguide/howto_properties.html"> Properties files</a> in
175 * special.
176 *
177 * @see java.util.Properties#load
178 */
179 public class PropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware {
180
181 /**
182 * <p>
183 * A default implementation of the {@code IOFactory} interface.
184 * </p>
185 * <p>
186 * This class implements the {@code createXXXX()} methods defined by the {@code IOFactory} interface in a way that the
187 * default objects (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are returned. Customizing either the
188 * reader or the writer (or both) can be done by extending this class and overriding the corresponding
189 * {@code createXXXX()} method.
190 * </p>
191 *
192 * @since 1.7
193 */
194 public static class DefaultIOFactory implements IOFactory {
195 /**
196 * The singleton instance.
197 */
198 static final DefaultIOFactory INSTANCE = new DefaultIOFactory();
199
200 /**
201 * Constructs a new instance.
202 */
203 public DefaultIOFactory() {
204 // empty
205 }
206
207 @Override
208 public PropertiesReader createPropertiesReader(final Reader in) {
209 return new PropertiesReader(in);
210 }
211
212 @Override
213 public PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler) {
214 return new PropertiesWriter(out, handler);
215 }
216 }
217
218 /**
219 * <p>
220 * Definition of an interface that allows customization of read and write operations.
221 * </p>
222 * <p>
223 * For reading and writing properties files the inner classes {@code PropertiesReader} and {@code PropertiesWriter} are
224 * used. This interface defines factory methods for creating both a {@code PropertiesReader} and a
225 * {@code PropertiesWriter}. An object implementing this interface can be passed to the {@code setIOFactory()} method of
226 * {@code PropertiesConfiguration}. Every time the configuration is read or written the {@code IOFactory} is asked to
227 * create the appropriate reader or writer object. This provides an opportunity to inject custom reader or writer
228 * implementations.
229 * </p>
230 *
231 * @since 1.7
232 */
233 public interface IOFactory {
234 /**
235 * Creates a {@code PropertiesReader} for reading a properties file. This method is called whenever the
236 * {@code PropertiesConfiguration} is loaded. The reader returned by this method is then used for parsing the properties
237 * file.
238 *
239 * @param in the underlying reader (of the properties file)
240 * @return the {@code PropertiesReader} for loading the configuration
241 */
242 PropertiesReader createPropertiesReader(Reader in);
243
244 /**
245 * Creates a {@code PropertiesWriter} for writing a properties file. This method is called before the
246 * {@code PropertiesConfiguration} is saved. The writer returned by this method is then used for writing the properties
247 * file.
248 *
249 * @param out the underlying writer (to the properties file)
250 * @param handler the list delimiter delimiter for list parsing
251 * @return the {@code PropertiesWriter} for saving the configuration
252 */
253 PropertiesWriter createPropertiesWriter(Writer out, ListDelimiterHandler handler);
254 }
255
256 /**
257 * An alternative {@link IOFactory} that tries to mimic the behavior of {@link java.util.Properties} (Jup) more closely.
258 * The goal is to allow both of them be used interchangeably when reading and writing properties files without losing or
259 * changing information.
260 * <p>
261 * It also has the option to <em>not</em> use Unicode escapes. When using UTF-8 encoding (which is for example the new default
262 * for resource bundle properties files since Java 9), Unicode escapes are no longer required and avoiding them makes
263 * properties files more readable with regular text editors.
264 * <p>
265 * Some of the ways this implementation differs from {@link DefaultIOFactory}:
266 * <ul>
267 * <li>Trailing whitespace will not be trimmed from each line.</li>
268 * <li>Unknown escape sequences will have their backslash removed.</li>
269 * <li>{@code \b} is not a recognized escape sequence.</li>
270 * <li>Leading spaces in property values are preserved by escaping them.</li>
271 * <li>All natural lines (i.e. in the file) of a logical property line will have their leading whitespace trimmed.</li>
272 * <li>Natural lines that look like comment lines within a logical line are not treated as such; they're part of the
273 * property value.</li>
274 * </ul>
275 *
276 * @since 2.4
277 */
278 public static class JupIOFactory implements IOFactory {
279
280 /**
281 * Whether characters less than {@code \u0020} and characters greater than {@code \u007E} in property keys or values
282 * should be escaped using Unicode escape sequences. Not necessary when for example writing as UTF-8.
283 */
284 private final boolean escapeUnicode;
285
286 /**
287 * Constructs a new {@link JupIOFactory} with Unicode escaping.
288 */
289 public JupIOFactory() {
290 this(true);
291 }
292
293 /**
294 * Constructs a new {@link JupIOFactory} with optional Unicode escaping. Whether Unicode escaping is required depends on
295 * the encoding used to save the properties file. E.g. for ISO-8859-1 this must be turned on, for UTF-8 it's not
296 * necessary. Unfortunately this factory can't determine the encoding on its own.
297 *
298 * @param escapeUnicode whether Unicode characters should be escaped
299 */
300 public JupIOFactory(final boolean escapeUnicode) {
301 this.escapeUnicode = escapeUnicode;
302 }
303
304 @Override
305 public PropertiesReader createPropertiesReader(final Reader in) {
306 return new JupPropertiesReader(in);
307 }
308
309 @Override
310 public PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler) {
311 return new JupPropertiesWriter(out, handler, escapeUnicode);
312 }
313
314 }
315
316 /**
317 * A {@link PropertiesReader} that tries to mimic the behavior of {@link java.util.Properties}.
318 *
319 * @since 2.4
320 */
321 public static class JupPropertiesReader extends PropertiesReader {
322
323 /**
324 * Constructs a new instance.
325 *
326 * @param reader A Reader.
327 */
328 public JupPropertiesReader(final Reader reader) {
329 super(reader);
330 }
331
332 @Override
333 protected void parseProperty(final String line) {
334 final String[] property = doParseProperty(line, false);
335 initPropertyName(property[0]);
336 initPropertyValue(property[1]);
337 initPropertySeparator(property[2]);
338 }
339
340 @Override
341 public String readProperty() throws IOException {
342 getCommentLines().clear();
343 final StringBuilder buffer = new StringBuilder();
344
345 while (true) {
346 String line = readLine();
347 if (line == null) {
348 // EOF
349 if (buffer.length() > 0) {
350 break;
351 }
352 return null;
353 }
354
355 // while a property line continues there are no comments (even if the line from
356 // the file looks like one)
357 if (isCommentLine(line) && buffer.length() == 0) {
358 getCommentLines().add(line);
359 continue;
360 }
361
362 // while property line continues left trim all following lines read from the
363 // file
364 if (buffer.length() > 0) {
365 // index of the first non-whitespace character
366 int i;
367 for (i = 0; i < line.length(); i++) {
368 if (!Character.isWhitespace(line.charAt(i))) {
369 break;
370 }
371 }
372
373 line = line.substring(i);
374 }
375
376 if (!checkCombineLines(line)) {
377 buffer.append(line);
378 break;
379 }
380 line = line.substring(0, line.length() - 1);
381 buffer.append(line);
382 }
383 return buffer.toString();
384 }
385
386 @Override
387 protected String unescapePropertyValue(final String value) {
388 return unescapeJava(value, true);
389 }
390
391 }
392
393 /**
394 * A {@link PropertiesWriter} that tries to mimic the behavior of {@link java.util.Properties}.
395 *
396 * @since 2.4
397 */
398 public static class JupPropertiesWriter extends PropertiesWriter {
399
400 /**
401 * The starting ASCII printable character.
402 */
403 private static final int PRINTABLE_INDEX_END = 0x7e;
404
405 /**
406 * The ending ASCII printable character.
407 */
408 private static final int PRINTABLE_INDEX_START = 0x20;
409
410 /**
411 * A UnicodeEscaper for characters outside the ASCII printable range.
412 */
413 private static final UnicodeEscaper ESCAPER = UnicodeEscaper.outsideOf(PRINTABLE_INDEX_START, PRINTABLE_INDEX_END);
414
415 /**
416 * Characters that need to be escaped when wring a properties file.
417 */
418 private static final Map<CharSequence, CharSequence> JUP_CHARS_ESCAPE;
419 static {
420 final Map<CharSequence, CharSequence> initialMap = new HashMap<>();
421 initialMap.put("\\", "\\\\");
422 initialMap.put("\n", "\\n");
423 initialMap.put("\t", "\\t");
424 initialMap.put("\f", "\\f");
425 initialMap.put("\r", "\\r");
426 JUP_CHARS_ESCAPE = Collections.unmodifiableMap(initialMap);
427 }
428
429 /**
430 * Creates a new instance of {@code JupPropertiesWriter}.
431 *
432 * @param writer a Writer object providing the underlying stream
433 * @param delHandler the delimiter handler for dealing with properties with multiple values
434 * @param escapeUnicode whether Unicode characters should be escaped using Unicode escapes
435 */
436 public JupPropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler, final boolean escapeUnicode) {
437 super(writer, delHandler, value -> {
438 String valueString = String.valueOf(value);
439
440 final CharSequenceTranslator translator;
441 if (escapeUnicode) {
442 translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE), ESCAPER);
443 } else {
444 translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE));
445 }
446
447 valueString = translator.translate(valueString);
448
449 // escape the first leading space to preserve it (and all after it)
450 if (valueString.startsWith(" ")) {
451 valueString = "\\" + valueString;
452 }
453
454 return valueString;
455 });
456 }
457
458 }
459
460 /**
461 * This class is used to read properties lines. These lines do not terminate with new-line chars but rather when there
462 * is no backslash sign a the end of the line. This is used to concatenate multiple lines for readability.
463 */
464 public static class PropertiesReader extends LineNumberReader {
465
466 /** The regular expression to parse the key and the value of a property. */
467 private static final Pattern PROPERTY_PATTERN = Pattern
468 .compile("(([\\S&&[^\\\\" + new String(SEPARATORS) + "]]|\\\\.)*+)(\\s*(\\s+|[" + new String(SEPARATORS) + "])\\s*)?(.*)");
469
470 /** Constant for the index of the group for the key. */
471 private static final int IDX_KEY = 1;
472
473 /** Constant for the index of the group for the value. */
474 private static final int IDX_VALUE = 5;
475
476 /** Constant for the index of the group for the separator. */
477 private static final int IDX_SEPARATOR = 3;
478
479 /**
480 * Checks if the passed in line should be combined with the following. This is true, if the line ends with an odd number
481 * of backslashes.
482 *
483 * @param line the line
484 * @return a flag if the lines should be combined
485 */
486 static boolean checkCombineLines(final String line) {
487 return countTrailingBS(line) % 2 != 0;
488 }
489
490 /**
491 * Parse a property line and return the key, the value, and the separator in an array.
492 *
493 * @param line the line to parse
494 * @param trimValue flag whether the value is to be trimmed
495 * @return an array with the property's key, value, and separator
496 */
497 static String[] doParseProperty(final String line, final boolean trimValue) {
498 final Matcher matcher = PROPERTY_PATTERN.matcher(line);
499
500 final String[] result = {"", "", ""};
501
502 if (matcher.matches()) {
503 result[0] = matcher.group(IDX_KEY).trim();
504
505 String value = matcher.group(IDX_VALUE);
506 if (trimValue) {
507 value = value.trim();
508 }
509 result[1] = value;
510
511 result[2] = matcher.group(IDX_SEPARATOR);
512 }
513
514 return result;
515 }
516
517 /** Stores the comment lines for the currently processed property. */
518 private final List<String> commentLines;
519
520 /** Stores the name of the last read property. */
521 private String propertyName;
522
523 /** Stores the value of the last read property. */
524 private String propertyValue;
525
526 /** Stores the property separator of the last read property. */
527 private String propertySeparator = DEFAULT_SEPARATOR;
528
529 /**
530 * Constructs a new instance.
531 *
532 * @param reader A Reader.
533 */
534 public PropertiesReader(final Reader reader) {
535 super(reader);
536 commentLines = new ArrayList<>();
537 }
538
539 /**
540 * Gets the comment lines that have been read for the last property.
541 *
542 * @return the comment lines for the last property returned by {@code readProperty()}
543 * @since 1.3
544 */
545 public List<String> getCommentLines() {
546 return commentLines;
547 }
548
549 /**
550 * Gets the name of the last read property. This method can be called after {@link #nextProperty()} was invoked and
551 * its return value was <strong>true</strong>.
552 *
553 * @return the name of the last read property
554 * @since 1.3
555 */
556 public String getPropertyName() {
557 return propertyName;
558 }
559
560 /**
561 * Gets the separator that was used for the last read property. The separator can be stored so that it can later be
562 * restored when saving the configuration.
563 *
564 * @return the separator for the last read property
565 * @since 1.7
566 */
567 public String getPropertySeparator() {
568 return propertySeparator;
569 }
570
571 /**
572 * Gets the value of the last read property. This method can be called after {@link #nextProperty()} was invoked and
573 * its return value was <strong>true</strong>.
574 *
575 * @return the value of the last read property
576 * @since 1.3
577 */
578 public String getPropertyValue() {
579 return propertyValue;
580 }
581
582 /**
583 * Sets the name of the current property. This method can be called by {@code parseProperty()} for storing the results
584 * of the parse operation. It also ensures that the property key is correctly escaped.
585 *
586 * @param name the name of the current property
587 * @since 1.7
588 */
589 protected void initPropertyName(final String name) {
590 propertyName = unescapePropertyName(name);
591 }
592
593 /**
594 * Sets the separator of the current property. This method can be called by {@code parseProperty()}. It allows the
595 * associated layout object to keep track of the property separators. When saving the configuration the separators can
596 * be restored.
597 *
598 * @param value the separator used for the current property
599 * @since 1.7
600 */
601 protected void initPropertySeparator(final String value) {
602 propertySeparator = value;
603 }
604
605 /**
606 * Sets the value of the current property. This method can be called by {@code parseProperty()} for storing the results
607 * of the parse operation. It also ensures that the property value is correctly escaped.
608 *
609 * @param value the value of the current property
610 * @since 1.7
611 */
612 protected void initPropertyValue(final String value) {
613 propertyValue = unescapePropertyValue(value);
614 }
615
616 /**
617 * Parses the next property from the input stream and stores the found name and value in internal fields. These fields
618 * can be obtained using the provided getter methods. The return value indicates whether EOF was reached (<strong>false</strong>)
619 * or whether further properties are available (<strong>true</strong>).
620 *
621 * @return a flag if further properties are available
622 * @throws IOException if an error occurs
623 * @since 1.3
624 */
625 public boolean nextProperty() throws IOException {
626 final String line = readProperty();
627
628 if (line == null) {
629 return false; // EOF
630 }
631
632 // parse the line
633 parseProperty(line);
634 return true;
635 }
636
637 /**
638 * Parses a line read from the properties file. This method is called for each non-comment line read from the source
639 * file. Its task is to split the passed in line into the property key and its value. The results of the parse operation
640 * can be stored by calling the {@code initPropertyXXX()} methods.
641 *
642 * @param line the line read from the properties file
643 * @since 1.7
644 */
645 protected void parseProperty(final String line) {
646 final String[] property = doParseProperty(line, true);
647 initPropertyName(property[0]);
648 initPropertyValue(property[1]);
649 initPropertySeparator(property[2]);
650 }
651
652 /**
653 * Reads a property line. Returns null if Stream is at EOF. Concatenates lines ending with "\". Skips lines beginning
654 * with "#" or "!" and empty lines. The return value is a property definition ({@code <name>} =
655 * {@code <value>})
656 *
657 * @return A string containing a property value or null
658 * @throws IOException in case of an I/O error
659 */
660 public String readProperty() throws IOException {
661 commentLines.clear();
662 final StringBuilder buffer = new StringBuilder();
663
664 while (true) {
665 String line = readLine();
666 if (line == null) {
667 // EOF
668 return null;
669 }
670
671 if (isCommentLine(line)) {
672 commentLines.add(line);
673 continue;
674 }
675
676 line = line.trim();
677
678 if (!checkCombineLines(line)) {
679 buffer.append(line);
680 break;
681 }
682 line = line.substring(0, line.length() - 1);
683 buffer.append(line);
684 }
685 return buffer.toString();
686 }
687
688 /**
689 * Performs unescaping on the given property name.
690 *
691 * @param name the property name
692 * @return the unescaped property name
693 * @since 2.4
694 */
695 protected String unescapePropertyName(final String name) {
696 return StringEscapeUtils.unescapeJava(name);
697 }
698
699 /**
700 * Performs unescaping on the given property value.
701 *
702 * @param value the property value
703 * @return the unescaped property value
704 * @since 2.4
705 */
706 protected String unescapePropertyValue(final String value) {
707 return unescapeJava(value);
708 }
709 } // class PropertiesReader
710
711 /**
712 * This class is used to write properties lines. The most important method is
713 * {@code writeProperty(String, Object, boolean)}, which is called during a save operation for each property found in
714 * the configuration.
715 */
716 public static class PropertiesWriter extends FilterWriter {
717
718 /**
719 * Properties escape map.
720 */
721 private static final Map<CharSequence, CharSequence> PROPERTIES_CHARS_ESCAPE;
722 static {
723 final Map<CharSequence, CharSequence> initialMap = new HashMap<>();
724 initialMap.put("\\", "\\\\");
725 PROPERTIES_CHARS_ESCAPE = Collections.unmodifiableMap(initialMap);
726 }
727
728 /**
729 * A translator for escaping property values. This translator performs a subset of transformations done by the
730 * ESCAPE_JAVA translator from Commons Lang 3.
731 */
732 private static final CharSequenceTranslator ESCAPE_PROPERTIES = new AggregateTranslator(new LookupTranslator(PROPERTIES_CHARS_ESCAPE),
733 new LookupTranslator(EntityArrays.JAVA_CTRL_CHARS_ESCAPE), UnicodeEscaper.outsideOf(32, 0x7f));
734
735 /**
736 * A {@code ValueTransformer} implementation used to escape property values. This implementation applies the
737 * transformation defined by the {@link #ESCAPE_PROPERTIES} translator.
738 */
739 private static final ValueTransformer DEFAULT_TRANSFORMER = value -> {
740 final String strVal = String.valueOf(value);
741 return ESCAPE_PROPERTIES.translate(strVal);
742 };
743
744 /** The value transformer used for escaping property values. */
745 private final ValueTransformer valueTransformer;
746
747 /** The list delimiter handler. */
748 private final ListDelimiterHandler delimiterHandler;
749
750 /** The separator to be used for the current property. */
751 private String currentSeparator;
752
753 /** The global separator. If set, it overrides the current separator. */
754 private String globalSeparator;
755
756 /** The line separator. */
757 private String lineSeparator;
758
759 /**
760 * Creates a new instance of {@code PropertiesWriter}.
761 *
762 * @param writer a Writer object providing the underlying stream
763 * @param delHandler the delimiter handler for dealing with properties with multiple values
764 */
765 public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler) {
766 this(writer, delHandler, DEFAULT_TRANSFORMER);
767 }
768
769 /**
770 * Creates a new instance of {@code PropertiesWriter}.
771 *
772 * @param writer a Writer object providing the underlying stream
773 * @param delHandler the delimiter handler for dealing with properties with multiple values
774 * @param valueTransformer the value transformer used to escape property values
775 */
776 public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler, final ValueTransformer valueTransformer) {
777 super(writer);
778 delimiterHandler = delHandler;
779 this.valueTransformer = valueTransformer;
780 }
781
782 /**
783 * Escapes the key of a property before it gets written to file. This method is called on saving a configuration for
784 * each property key. It ensures that separator characters contained in the key are escaped.
785 *
786 * @param key the key
787 * @return the escaped key
788 * @since 2.0
789 */
790 protected String escapeKey(final String key) {
791 final StringBuilder newkey = new StringBuilder();
792
793 for (int i = 0; i < key.length(); i++) {
794 final char c = key.charAt(i);
795
796 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c) || c == '\\') {
797 // escape the separator
798 newkey.append('\\');
799 }
800 newkey.append(c);
801 }
802
803 return newkey.toString();
804 }
805
806 /**
807 * Returns the separator to be used for the given property. This method is called by {@code writeProperty()}. The string
808 * returned here is used as separator between the property key and its value. Per default the method checks whether a
809 * global separator is set. If this is the case, it is returned. Otherwise the separator returned by
810 * {@code getCurrentSeparator()} is used, which was set by the associated layout object. Derived classes may implement a
811 * different strategy for defining the separator.
812 *
813 * @param key the property key
814 * @param value the value
815 * @return the separator to be used
816 * @since 1.7
817 */
818 protected String fetchSeparator(final String key, final Object value) {
819 return getGlobalSeparator() != null ? getGlobalSeparator() : StringUtils.defaultString(getCurrentSeparator());
820 }
821
822 /**
823 * Gets the current property separator.
824 *
825 * @return the current property separator
826 * @since 1.7
827 */
828 public String getCurrentSeparator() {
829 return currentSeparator;
830 }
831
832 /**
833 * Gets the delimiter handler for properties with multiple values. This object is used to escape property values so
834 * that they can be read in correctly the next time they are loaded.
835 *
836 * @return the delimiter handler for properties with multiple values
837 * @since 2.0
838 */
839 public ListDelimiterHandler getDelimiterHandler() {
840 return delimiterHandler;
841 }
842
843 /**
844 * Gets the global property separator.
845 *
846 * @return the global property separator
847 * @since 1.7
848 */
849 public String getGlobalSeparator() {
850 return globalSeparator;
851 }
852
853 /**
854 * Gets the line separator.
855 *
856 * @return the line separator
857 * @since 1.7
858 */
859 public String getLineSeparator() {
860 return lineSeparator != null ? lineSeparator : LINE_SEPARATOR;
861 }
862
863 /**
864 * Sets the current property separator. This separator is used when writing the next property.
865 *
866 * @param currentSeparator the current property separator
867 * @since 1.7
868 */
869 public void setCurrentSeparator(final String currentSeparator) {
870 this.currentSeparator = currentSeparator;
871 }
872
873 /**
874 * Sets the global property separator. This separator corresponds to the {@code globalSeparator} property of
875 * {@link PropertiesConfigurationLayout}. It defines the separator to be used for all properties. If it is undefined,
876 * the current separator is used.
877 *
878 * @param globalSeparator the global property separator
879 * @since 1.7
880 */
881 public void setGlobalSeparator(final String globalSeparator) {
882 this.globalSeparator = globalSeparator;
883 }
884
885 /**
886 * Sets the line separator. Each line written by this writer is terminated with this separator. If not set, the
887 * platform-specific line separator is used.
888 *
889 * @param lineSeparator the line separator to be used
890 * @since 1.7
891 */
892 public void setLineSeparator(final String lineSeparator) {
893 this.lineSeparator = lineSeparator;
894 }
895
896 /**
897 * Writes a comment.
898 *
899 * @param comment the comment to write
900 * @throws IOException if an I/O error occurs.
901 */
902 public void writeComment(final String comment) throws IOException {
903 writeln("# " + comment);
904 }
905
906 /**
907 * Helper method for writing a line with the platform specific line ending.
908 *
909 * @param s the content of the line (may be <strong>null</strong>)
910 * @throws IOException if an error occurs
911 * @since 1.3
912 */
913 public void writeln(final String s) throws IOException {
914 if (s != null) {
915 write(s);
916 }
917 write(getLineSeparator());
918 }
919
920 /**
921 * Writes a property.
922 *
923 * @param key The key of the property
924 * @param values The array of values of the property
925 * @throws IOException if an I/O error occurs.
926 */
927 public void writeProperty(final String key, final List<?> values) throws IOException {
928 for (final Object value : values) {
929 writeProperty(key, value);
930 }
931 }
932
933 /**
934 * Writes a property.
935 *
936 * @param key the key of the property
937 * @param value the value of the property
938 * @throws IOException if an I/O error occurs.
939 */
940 public void writeProperty(final String key, final Object value) throws IOException {
941 writeProperty(key, value, false);
942 }
943
944 /**
945 * Writes the given property and its value. If the value happens to be a list, the {@code forceSingleLine} flag is
946 * evaluated. If it is set, all values are written on a single line using the list delimiter as separator.
947 *
948 * @param key the property key
949 * @param value the property value
950 * @param forceSingleLine the "force single line" flag
951 * @throws IOException if an error occurs
952 * @since 1.3
953 */
954 public void writeProperty(final String key, final Object value, final boolean forceSingleLine) throws IOException {
955 String v;
956
957 if (value instanceof List) {
958 v = null;
959 final List<?> values = (List<?>) value;
960 if (forceSingleLine) {
961 try {
962 v = String.valueOf(getDelimiterHandler().escapeList(values, valueTransformer));
963 } catch (final UnsupportedOperationException ignored) {
964 // the handler may not support escaping lists,
965 // then the list is written in multiple lines
966 }
967 }
968 if (v == null) {
969 writeProperty(key, values);
970 return;
971 }
972 } else {
973 v = String.valueOf(getDelimiterHandler().escape(value, valueTransformer));
974 }
975
976 write(escapeKey(key));
977 write(fetchSeparator(key, value));
978 write(v);
979
980 writeln(null);
981 }
982 } // class PropertiesWriter
983
984 /**
985 * Defines default error handling for the special {@code "include"} key by throwing the given exception.
986 *
987 * @since 2.6
988 */
989 public static final ConfigurationConsumer<ConfigurationException> DEFAULT_INCLUDE_LISTENER = e -> {
990 throw e;
991 };
992
993 /**
994 * Defines error handling as a noop for the special {@code "include"} key.
995 *
996 * @since 2.6
997 */
998 public static final ConfigurationConsumer<ConfigurationException> NOOP_INCLUDE_LISTENER = e -> { /* noop */ };
999
1000 /**
1001 * The default encoding (ISO-8859-1 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html)
1002 */
1003 public static final String DEFAULT_ENCODING = StandardCharsets.ISO_8859_1.name();
1004
1005 /** Constant for the supported comment characters. */
1006 static final String COMMENT_CHARS = "#!";
1007
1008 /** Constant for the default properties separator. */
1009 static final String DEFAULT_SEPARATOR = " = ";
1010
1011 /**
1012 * A string with special characters that need to be unescaped when reading a properties file.
1013 * {@link java.util.Properties} escapes these characters when writing out a properties file.
1014 */
1015 private static final String UNESCAPE_CHARACTERS = ":#=!\\\'\"";
1016
1017 /**
1018 * This is the name of the property that can point to other properties file for including other properties files.
1019 */
1020 private static String include = "include";
1021
1022 /**
1023 * This is the name of the property that can point to other properties file for including other properties files.
1024 * <p>
1025 * If the file is absent, processing continues normally.
1026 * </p>
1027 */
1028 private static String includeOptional = "includeoptional";
1029
1030 /** The list of possible key/value separators */
1031 private static final char[] SEPARATORS = {'=', ':'};
1032
1033 /** The white space characters used as key/value separators. */
1034 private static final char[] WHITE_SPACE = {' ', '\t', '\f'};
1035
1036 /** Constant for the platform specific line separator. */
1037 private static final String LINE_SEPARATOR = System.lineSeparator();
1038
1039 /** Constant for the radix of hex numbers. */
1040 private static final int HEX_RADIX = 16;
1041
1042 /** Constant for the length of a unicode literal. */
1043 private static final int UNICODE_LEN = 4;
1044
1045 /**
1046 * Returns the number of trailing backslashes. This is sometimes needed for the correct handling of escape characters.
1047 *
1048 * @param line the string to investigate
1049 * @return the number of trailing backslashes
1050 */
1051 private static int countTrailingBS(final String line) {
1052 int bsCount = 0;
1053 for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) {
1054 bsCount++;
1055 }
1056
1057 return bsCount;
1058 }
1059
1060 /**
1061 * Gets the property value for including other properties files. By default it is "include".
1062 *
1063 * @return A String.
1064 */
1065 public static String getInclude() {
1066 return include;
1067 }
1068
1069 /**
1070 * Gets the property value for including other properties files. By default it is "includeoptional".
1071 * <p>
1072 * If the file is absent, processing continues normally.
1073 * </p>
1074 *
1075 * @return A String.
1076 * @since 2.5
1077 */
1078 public static String getIncludeOptional() {
1079 return includeOptional;
1080 }
1081
1082 /**
1083 * Tests whether a line is a comment, i.e. whether it starts with a comment character.
1084 *
1085 * @param line the line
1086 * @return a flag if this is a comment line
1087 * @since 1.3
1088 */
1089 static boolean isCommentLine(final String line) {
1090 final String s = line.trim();
1091 // blank lines are also treated as comment lines
1092 return s.isEmpty() || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
1093 }
1094
1095 /**
1096 * Checks whether the specified character needs to be unescaped. This method is called when during reading a property
1097 * file an escape character ('\') is detected. If the character following the escape character is recognized as a
1098 * special character which is escaped per default in a Java properties file, it has to be unescaped.
1099 *
1100 * @param ch the character in question
1101 * @return a flag whether this character has to be unescaped
1102 */
1103 private static boolean needsUnescape(final char ch) {
1104 return UNESCAPE_CHARACTERS.indexOf(ch) >= 0;
1105 }
1106
1107 /**
1108 * Sets the property value for including other properties files. By default it is "include".
1109 *
1110 * @param inc A String.
1111 */
1112 public static void setInclude(final String inc) {
1113 include = inc;
1114 }
1115
1116 /**
1117 * Sets the property value for including other properties files. By default it is "include".
1118 * <p>
1119 * If the file is absent, processing continues normally.
1120 * </p>
1121 *
1122 * @param inc A String.
1123 * @since 2.5
1124 */
1125 public static void setIncludeOptional(final String inc) {
1126 includeOptional = inc;
1127 }
1128
1129 /**
1130 * <p>
1131 * Unescapes any Java literals found in the {@code String} to a {@code Writer}.
1132 * </p>
1133 * This is a slightly modified version of the StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
1134 * drop escaped separators (i.e '\,').
1135 *
1136 * @param str the {@code String} to unescape, may be null
1137 * @return the processed string
1138 * @throws IllegalArgumentException if the Writer is {@code null}
1139 */
1140 protected static String unescapeJava(final String str) {
1141 return unescapeJava(str, false);
1142 }
1143
1144 /**
1145 * Unescapes Java literals found in the {@code String} to a {@code Writer}.
1146 * <p>
1147 * When the parameter {@code jupCompatible} is {@code false}, the classic behavior is used (see
1148 * {@link #unescapeJava(String)}). When it's {@code true} a slightly different behavior that's compatible with
1149 * {@link java.util.Properties} is used (see {@link JupIOFactory}).
1150 * </p>
1151 *
1152 * @param str the {@code String} to unescape, may be null
1153 * @param jupCompatible whether unescaping is compatible with {@link java.util.Properties}; otherwise the classic
1154 * behavior is used
1155 * @return the processed string
1156 * @throws IllegalArgumentException if the Writer is {@code null}
1157 */
1158 protected static String unescapeJava(final String str, final boolean jupCompatible) {
1159 if (str == null) {
1160 return null;
1161 }
1162 final int sz = str.length();
1163 final StringBuilder out = new StringBuilder(sz);
1164 final StringBuilder unicode = new StringBuilder(UNICODE_LEN);
1165 boolean hadSlash = false;
1166 boolean inUnicode = false;
1167 for (int i = 0; i < sz; i++) {
1168 final char ch = str.charAt(i);
1169 if (inUnicode) {
1170 // if in unicode, then we're reading unicode
1171 // values in somehow
1172 unicode.append(ch);
1173 if (unicode.length() == UNICODE_LEN) {
1174 // unicode now contains the four hex digits
1175 // which represents our unicode character
1176 try {
1177 final int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
1178 out.append((char) value);
1179 unicode.setLength(0);
1180 inUnicode = false;
1181 hadSlash = false;
1182 } catch (final NumberFormatException nfe) {
1183 throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
1184 }
1185 }
1186 continue;
1187 }
1188
1189 if (hadSlash) {
1190 // handle an escaped value
1191 hadSlash = false;
1192
1193 switch (ch) {
1194 case 'r':
1195 out.append('\r');
1196 break;
1197 case 'f':
1198 out.append('\f');
1199 break;
1200 case 't':
1201 out.append('\t');
1202 break;
1203 case 'n':
1204 out.append('\n');
1205 break;
1206 default:
1207 if (!jupCompatible && ch == 'b') {
1208 out.append('\b');
1209 } else if (ch == 'u') {
1210 // uh-oh, we're in unicode country....
1211 inUnicode = true;
1212 } else {
1213 // JUP simply throws away the \ of unknown escape sequences
1214 if (!needsUnescape(ch) && !jupCompatible) {
1215 out.append('\\');
1216 }
1217 out.append(ch);
1218 }
1219 break;
1220 }
1221
1222 continue;
1223 }
1224 if (ch == '\\') {
1225 hadSlash = true;
1226 continue;
1227 }
1228 out.append(ch);
1229 }
1230
1231 if (hadSlash) {
1232 // then we're in the weird case of a \ at the end of the
1233 // string, let's output it anyway.
1234 out.append('\\');
1235 }
1236
1237 return out.toString();
1238 }
1239
1240 /** Stores the layout object. */
1241 private PropertiesConfigurationLayout layout;
1242
1243 /** The include listener for the special {@code "include"} key. */
1244 private ConfigurationConsumer<ConfigurationException> includeListener;
1245
1246 /** The IOFactory for creating readers and writers. */
1247 private IOFactory ioFactory;
1248
1249 /** The current {@code FileLocator}. */
1250 private FileLocator locator;
1251
1252 /** Allow file inclusion or not */
1253 private boolean includesAllowed = true;
1254
1255 /**
1256 * Creates an empty PropertyConfiguration object which can be used to synthesize a new Properties file by adding values
1257 * and then saving().
1258 */
1259 public PropertiesConfiguration() {
1260 installLayout(createLayout());
1261 }
1262
1263 /**
1264 * Creates a copy of this object.
1265 *
1266 * @return the copy
1267 */
1268 @Override
1269 public Object clone() {
1270 final PropertiesConfiguration copy = (PropertiesConfiguration) super.clone();
1271 if (layout != null) {
1272 copy.setLayout(new PropertiesConfigurationLayout(layout));
1273 }
1274 return copy;
1275 }
1276
1277 /**
1278 * Creates a standard layout object. This configuration is initialized with such a standard layout.
1279 *
1280 * @return the newly created layout object
1281 */
1282 private PropertiesConfigurationLayout createLayout() {
1283 return new PropertiesConfigurationLayout();
1284 }
1285
1286 /**
1287 * Gets the footer comment. This is a comment at the very end of the file.
1288 *
1289 * @return the footer comment
1290 * @since 2.0
1291 */
1292 public String getFooter() {
1293 return syncRead(() -> getLayout().getFooterComment(), false);
1294 }
1295
1296 /**
1297 * Gets the comment header.
1298 *
1299 * @return the comment header
1300 * @since 1.1
1301 */
1302 public String getHeader() {
1303 return syncRead(() -> getLayout().getHeaderComment(), false);
1304 }
1305
1306 /**
1307 * Gets the current include listener, never null.
1308 *
1309 * @return the current include listener, never null.
1310 * @since 2.6
1311 */
1312 public ConfigurationConsumer<ConfigurationException> getIncludeListener() {
1313 return includeListener != null ? includeListener : DEFAULT_INCLUDE_LISTENER;
1314 }
1315
1316 /**
1317 * Gets the {@code IOFactory} to be used for creating readers and writers when loading or saving this configuration.
1318 *
1319 * @return the {@code IOFactory}
1320 * @since 1.7
1321 */
1322 public IOFactory getIOFactory() {
1323 return ioFactory != null ? ioFactory : DefaultIOFactory.INSTANCE;
1324 }
1325
1326 /**
1327 * Gets the associated layout object.
1328 *
1329 * @return the associated layout object
1330 * @since 1.3
1331 */
1332 public PropertiesConfigurationLayout getLayout() {
1333 return layout;
1334 }
1335
1336 /**
1337 * Stores the current {@code FileLocator} for a following IO operation. The {@code FileLocator} is needed to resolve
1338 * include files with relative file names.
1339 *
1340 * @param locator the current {@code FileLocator}
1341 * @since 2.0
1342 */
1343 @Override
1344 public void initFileLocator(final FileLocator locator) {
1345 this.locator = locator;
1346 }
1347
1348 /**
1349 * Installs a layout object. It has to be ensured that the layout is registered as change listener at this
1350 * configuration. If there is already a layout object installed, it has to be removed properly.
1351 *
1352 * @param layout the layout object to be installed
1353 */
1354 private void installLayout(final PropertiesConfigurationLayout layout) {
1355 // only one layout must exist
1356 if (this.layout != null) {
1357 removeEventListener(ConfigurationEvent.ANY, this.layout);
1358 }
1359
1360 if (layout == null) {
1361 this.layout = createLayout();
1362 } else {
1363 this.layout = layout;
1364 }
1365 addEventListener(ConfigurationEvent.ANY, this.layout);
1366 }
1367
1368 /**
1369 * Reports the status of file inclusion.
1370 *
1371 * @return True if include files are loaded.
1372 */
1373 public boolean isIncludesAllowed() {
1374 return this.includesAllowed;
1375 }
1376
1377 /**
1378 * Helper method for loading an included properties file. This method is called by {@code load()} when an
1379 * {@code include} property is encountered. It tries to resolve relative file names based on the current base path. If
1380 * this fails, a resolution based on the location of this properties file is tried.
1381 *
1382 * @param fileName the name of the file to load
1383 * @param optional whether or not the {@code fileName} is optional
1384 * @param seenStack Stack of seen include URLs
1385 * @throws ConfigurationException if loading fails
1386 */
1387 private void loadIncludeFile(final String fileName, final boolean optional, final Deque<URL> seenStack) throws ConfigurationException {
1388 if (locator == null) {
1389 throw new ConfigurationException(
1390 "Load operation not properly initialized! Do not call read(InputStream) directly, but use a FileHandler to load a configuration.");
1391 }
1392
1393 URL url = locateIncludeFile(locator.getBasePath(), fileName);
1394 if (url == null) {
1395 final URL baseURL = locator.getSourceURL();
1396 if (baseURL != null) {
1397 url = locateIncludeFile(baseURL.toString(), fileName);
1398 }
1399 }
1400
1401 if (optional && url == null) {
1402 return;
1403 }
1404
1405 if (url == null) {
1406 getIncludeListener().accept(new ConfigurationException("Cannot resolve include file " + fileName, new FileNotFoundException(fileName)));
1407 } else {
1408 final FileHandler fh = new FileHandler(this);
1409 fh.setFileLocator(locator);
1410 final FileLocator orgLocator = locator;
1411 try {
1412 try {
1413 // Check for cycles
1414 if (seenStack.contains(url)) {
1415 throw new ConfigurationException(String.format("Cycle detected loading %s, seen stack: %s", url, seenStack));
1416 }
1417 seenStack.add(url);
1418 try {
1419 fh.load(url);
1420 } finally {
1421 seenStack.pop();
1422 }
1423 } catch (final ConfigurationException e) {
1424 getIncludeListener().accept(e);
1425 }
1426 } finally {
1427 locator = orgLocator; // reset locator which is changed by load
1428 }
1429 }
1430 }
1431
1432 /**
1433 * Tries to obtain the URL of an include file using the specified (optional) base path and file name.
1434 *
1435 * @param basePath the base path
1436 * @param fileName the file name
1437 * @return the URL of the include file or <strong>null</strong> if it cannot be resolved
1438 */
1439 private URL locateIncludeFile(final String basePath, final String fileName) {
1440 final FileLocator includeLocator = FileLocatorUtils.fileLocator(locator).sourceURL(null).basePath(basePath).fileName(fileName).create();
1441 return FileLocatorUtils.locate(includeLocator);
1442 }
1443
1444 /**
1445 * This method is invoked by the associated {@link PropertiesConfigurationLayout} object for each property definition
1446 * detected in the parsed properties file. Its task is to check whether this is a special property definition (for example the
1447 * {@code include} property). If not, the property must be added to this configuration. The return value indicates
1448 * whether the property should be treated as a normal property. If it is <strong>false</strong>, the layout object will ignore
1449 * this property.
1450 *
1451 * @param key the property key
1452 * @param value the property value
1453 * @param seenStack the stack of seen include URLs
1454 * @return a flag whether this is a normal property
1455 * @throws ConfigurationException if an error occurs
1456 * @since 1.3
1457 */
1458 boolean propertyLoaded(final String key, final String value, final Deque<URL> seenStack) throws ConfigurationException {
1459 final boolean result;
1460
1461 if (StringUtils.isNotEmpty(getInclude()) && key.equalsIgnoreCase(getInclude())) {
1462 if (isIncludesAllowed()) {
1463 final Collection<String> files = getListDelimiterHandler().split(value, true);
1464 for (final String f : files) {
1465 loadIncludeFile(interpolate(f), false, seenStack);
1466 }
1467 }
1468 result = false;
1469 } else if (StringUtils.isNotEmpty(getIncludeOptional()) && key.equalsIgnoreCase(getIncludeOptional())) {
1470 if (isIncludesAllowed()) {
1471 final Collection<String> files = getListDelimiterHandler().split(value, true);
1472 for (final String f : files) {
1473 loadIncludeFile(interpolate(f), true, seenStack);
1474 }
1475 }
1476 result = false;
1477 } else {
1478 addPropertyInternal(key, value);
1479 result = true;
1480 }
1481
1482 return result;
1483 }
1484
1485 /**
1486 * {@inheritDoc} This implementation delegates to the associated layout object which does the actual loading. Note that
1487 * this method does not do any synchronization. This lies in the responsibility of the caller. (Typically, the caller is
1488 * a {@code FileHandler} object which takes care for proper synchronization.)
1489 *
1490 * @since 2.0
1491 */
1492 @Override
1493 public void read(final Reader in) throws ConfigurationException, IOException {
1494 getLayout().load(this, in);
1495 }
1496
1497 /**
1498 * Sets the footer comment. If set, this comment is written after all properties at the end of the file.
1499 *
1500 * @param footer the footer comment
1501 * @since 2.0
1502 */
1503 public void setFooter(final String footer) {
1504 syncWrite(() -> getLayout().setFooterComment(footer), false);
1505 }
1506
1507 /**
1508 * Sets the comment header.
1509 *
1510 * @param header the header to use
1511 * @since 1.1
1512 */
1513 public void setHeader(final String header) {
1514 syncWrite(() -> getLayout().setHeaderComment(header), false);
1515 }
1516
1517 /**
1518 * Sets the current include listener, may not be null.
1519 *
1520 * @param includeListener the current include listener, may not be null.
1521 * @throws IllegalArgumentException if the {@code includeListener} is null.
1522 * @since 2.6
1523 */
1524 public void setIncludeListener(final ConfigurationConsumer<ConfigurationException> includeListener) {
1525 if (includeListener == null) {
1526 throw new IllegalArgumentException("includeListener must not be null.");
1527 }
1528 this.includeListener = includeListener;
1529 }
1530
1531 /**
1532 * Controls whether additional files can be loaded by the {@code include = <xxx>} statement or not. This is <strong>true</strong>
1533 * per default.
1534 *
1535 * @param includesAllowed True if Includes are allowed.
1536 */
1537 public void setIncludesAllowed(final boolean includesAllowed) {
1538 this.includesAllowed = includesAllowed;
1539 }
1540
1541 /**
1542 * Sets the {@code IOFactory} to be used for creating readers and writers when loading or saving this configuration.
1543 * Using this method a client can customize the reader and writer classes used by the load and save operations. Note
1544 * that this method must be called before invoking one of the {@code load()} and {@code save()} methods. Especially, if
1545 * you want to use a custom {@code IOFactory} for changing the {@code PropertiesReader}, you cannot load the
1546 * configuration data in the constructor.
1547 *
1548 * @param ioFactory the new {@code IOFactory} (must not be <strong>null</strong>)
1549 * @throws IllegalArgumentException if the {@code IOFactory} is <strong>null</strong>
1550 * @since 1.7
1551 */
1552 public void setIOFactory(final IOFactory ioFactory) {
1553 if (ioFactory == null) {
1554 throw new IllegalArgumentException("IOFactory must not be null.");
1555 }
1556
1557 this.ioFactory = ioFactory;
1558 }
1559
1560 /**
1561 * Sets the associated layout object.
1562 *
1563 * @param layout the new layout object; can be <strong>null</strong>, then a new layout object will be created
1564 * @since 1.3
1565 */
1566 public void setLayout(final PropertiesConfigurationLayout layout) {
1567 installLayout(layout);
1568 }
1569
1570 /**
1571 * {@inheritDoc} This implementation delegates to the associated layout object which does the actual saving. Note that,
1572 * analogous to {@link #read(Reader)}, this method does not do any synchronization.
1573 *
1574 * @since 2.0
1575 */
1576 @Override
1577 public void write(final Writer out) throws ConfigurationException, IOException {
1578 getLayout().save(this, out);
1579 }
1580
1581 }