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