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