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 <i>key</i> may use any character, separators must be escaped:
80   *
81   * <pre>
82   *  key\:foo = bar
83   * </pre>
84   *
85   * </li>
86   * <li><i>value</i> 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), <i>value</i> 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 <i>key</i> 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 e.g. 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 e.g. 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         /** Stores the comment lines for the currently processed property. */
473         private final List<String> commentLines;
474 
475         /** Stores the name of the last read property. */
476         private String propertyName;
477 
478         /** Stores the value of the last read property. */
479         private String propertyValue;
480 
481         /** Stores the property separator of the last read property. */
482         private String propertySeparator = DEFAULT_SEPARATOR;
483 
484         /**
485          * Constructs a new instance.
486          *
487          * @param reader A Reader.
488          */
489         public PropertiesReader(final Reader reader) {
490             super(reader);
491             commentLines = new ArrayList<>();
492         }
493 
494         /**
495          * Checks if the passed in line should be combined with the following. This is true, if the line ends with an odd number
496          * of backslashes.
497          *
498          * @param line the line
499          * @return a flag if the lines should be combined
500          */
501         static boolean checkCombineLines(final String line) {
502             return countTrailingBS(line) % 2 != 0;
503         }
504 
505         /**
506          * Parse a property line and return the key, the value, and the separator in an array.
507          *
508          * @param line the line to parse
509          * @param trimValue flag whether the value is to be trimmed
510          * @return an array with the property's key, value, and separator
511          */
512         static String[] doParseProperty(final String line, final boolean trimValue) {
513             final Matcher matcher = PROPERTY_PATTERN.matcher(line);
514 
515             final String[] result = {"", "", ""};
516 
517             if (matcher.matches()) {
518                 result[0] = matcher.group(IDX_KEY).trim();
519 
520                 String value = matcher.group(IDX_VALUE);
521                 if (trimValue) {
522                     value = value.trim();
523                 }
524                 result[1] = value;
525 
526                 result[2] = matcher.group(IDX_SEPARATOR);
527             }
528 
529             return result;
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 <b>true</b>.
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 <b>true</b>.
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 (<b>false</b>)
612          * or whether further properties are available (<b>true</b>).
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          *
652          * @throws IOException in case of an I/O error
653          */
654         public String readProperty() throws IOException {
655             commentLines.clear();
656             final StringBuilder buffer = new StringBuilder();
657 
658             while (true) {
659                 String line = readLine();
660                 if (line == null) {
661                     // EOF
662                     return null;
663                 }
664 
665                 if (isCommentLine(line)) {
666                     commentLines.add(line);
667                     continue;
668                 }
669 
670                 line = line.trim();
671 
672                 if (!checkCombineLines(line)) {
673                     buffer.append(line);
674                     break;
675                 }
676                 line = line.substring(0, line.length() - 1);
677                 buffer.append(line);
678             }
679             return buffer.toString();
680         }
681 
682         /**
683          * Performs unescaping on the given property name.
684          *
685          * @param name the property name
686          * @return the unescaped property name
687          * @since 2.4
688          */
689         protected String unescapePropertyName(final String name) {
690             return StringEscapeUtils.unescapeJava(name);
691         }
692 
693         /**
694          * Performs unescaping on the given property value.
695          *
696          * @param value the property value
697          * @return the unescaped property value
698          * @since 2.4
699          */
700         protected String unescapePropertyValue(final String value) {
701             return unescapeJava(value);
702         }
703     } // class PropertiesReader
704 
705     /**
706      * This class is used to write properties lines. The most important method is
707      * {@code writeProperty(String, Object, boolean)}, which is called during a save operation for each property found in
708      * the configuration.
709      */
710     public static class PropertiesWriter extends FilterWriter {
711 
712         /**
713          * Properties escape map.
714          */
715         private static final Map<CharSequence, CharSequence> PROPERTIES_CHARS_ESCAPE;
716         static {
717             final Map<CharSequence, CharSequence> initialMap = new HashMap<>();
718             initialMap.put("\\", "\\\\");
719             PROPERTIES_CHARS_ESCAPE = Collections.unmodifiableMap(initialMap);
720         }
721 
722         /**
723          * A translator for escaping property values. This translator performs a subset of transformations done by the
724          * ESCAPE_JAVA translator from Commons Lang 3.
725          */
726         private static final CharSequenceTranslator ESCAPE_PROPERTIES = new AggregateTranslator(new LookupTranslator(PROPERTIES_CHARS_ESCAPE),
727             new LookupTranslator(EntityArrays.JAVA_CTRL_CHARS_ESCAPE), UnicodeEscaper.outsideOf(32, 0x7f));
728 
729         /**
730          * A {@code ValueTransformer} implementation used to escape property values. This implementation applies the
731          * transformation defined by the {@link #ESCAPE_PROPERTIES} translator.
732          */
733         private static final ValueTransformer DEFAULT_TRANSFORMER = value -> {
734             final String strVal = String.valueOf(value);
735             return ESCAPE_PROPERTIES.translate(strVal);
736         };
737 
738         /** The value transformer used for escaping property values. */
739         private final ValueTransformer valueTransformer;
740 
741         /** The list delimiter handler. */
742         private final ListDelimiterHandler delimiterHandler;
743 
744         /** The separator to be used for the current property. */
745         private String currentSeparator;
746 
747         /** The global separator. If set, it overrides the current separator. */
748         private String globalSeparator;
749 
750         /** The line separator. */
751         private String lineSeparator;
752 
753         /**
754          * Creates a new instance of {@code PropertiesWriter}.
755          *
756          * @param writer a Writer object providing the underlying stream
757          * @param delHandler the delimiter handler for dealing with properties with multiple values
758          */
759         public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler) {
760             this(writer, delHandler, DEFAULT_TRANSFORMER);
761         }
762 
763         /**
764          * Creates a new instance of {@code PropertiesWriter}.
765          *
766          * @param writer a Writer object providing the underlying stream
767          * @param delHandler the delimiter handler for dealing with properties with multiple values
768          * @param valueTransformer the value transformer used to escape property values
769          */
770         public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler, final ValueTransformer valueTransformer) {
771             super(writer);
772             delimiterHandler = delHandler;
773             this.valueTransformer = valueTransformer;
774         }
775 
776         /**
777          * Escapes the key of a property before it gets written to file. This method is called on saving a configuration for
778          * each property key. It ensures that separator characters contained in the key are escaped.
779          *
780          * @param key the key
781          * @return the escaped key
782          * @since 2.0
783          */
784         protected String escapeKey(final String key) {
785             final StringBuilder newkey = new StringBuilder();
786 
787             for (int i = 0; i < key.length(); i++) {
788                 final char c = key.charAt(i);
789 
790                 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c) || c == '\\') {
791                     // escape the separator
792                     newkey.append('\\');
793                 }
794                 newkey.append(c);
795             }
796 
797             return newkey.toString();
798         }
799 
800         /**
801          * Returns the separator to be used for the given property. This method is called by {@code writeProperty()}. The string
802          * returned here is used as separator between the property key and its value. Per default the method checks whether a
803          * global separator is set. If this is the case, it is returned. Otherwise the separator returned by
804          * {@code getCurrentSeparator()} is used, which was set by the associated layout object. Derived classes may implement a
805          * different strategy for defining the separator.
806          *
807          * @param key the property key
808          * @param value the value
809          * @return the separator to be used
810          * @since 1.7
811          */
812         protected String fetchSeparator(final String key, final Object value) {
813             return getGlobalSeparator() != null ? getGlobalSeparator() : StringUtils.defaultString(getCurrentSeparator());
814         }
815 
816         /**
817          * Gets the current property separator.
818          *
819          * @return the current property separator
820          * @since 1.7
821          */
822         public String getCurrentSeparator() {
823             return currentSeparator;
824         }
825 
826         /**
827          * Gets the delimiter handler for properties with multiple values. This object is used to escape property values so
828          * that they can be read in correctly the next time they are loaded.
829          *
830          * @return the delimiter handler for properties with multiple values
831          * @since 2.0
832          */
833         public ListDelimiterHandler getDelimiterHandler() {
834             return delimiterHandler;
835         }
836 
837         /**
838          * Gets the global property separator.
839          *
840          * @return the global property separator
841          * @since 1.7
842          */
843         public String getGlobalSeparator() {
844             return globalSeparator;
845         }
846 
847         /**
848          * Gets the line separator.
849          *
850          * @return the line separator
851          * @since 1.7
852          */
853         public String getLineSeparator() {
854             return lineSeparator != null ? lineSeparator : LINE_SEPARATOR;
855         }
856 
857         /**
858          * Sets the current property separator. This separator is used when writing the next property.
859          *
860          * @param currentSeparator the current property separator
861          * @since 1.7
862          */
863         public void setCurrentSeparator(final String currentSeparator) {
864             this.currentSeparator = currentSeparator;
865         }
866 
867         /**
868          * Sets the global property separator. This separator corresponds to the {@code globalSeparator} property of
869          * {@link PropertiesConfigurationLayout}. It defines the separator to be used for all properties. If it is undefined,
870          * the current separator is used.
871          *
872          * @param globalSeparator the global property separator
873          * @since 1.7
874          */
875         public void setGlobalSeparator(final String globalSeparator) {
876             this.globalSeparator = globalSeparator;
877         }
878 
879         /**
880          * Sets the line separator. Each line written by this writer is terminated with this separator. If not set, the
881          * platform-specific line separator is used.
882          *
883          * @param lineSeparator the line separator to be used
884          * @since 1.7
885          */
886         public void setLineSeparator(final String lineSeparator) {
887             this.lineSeparator = lineSeparator;
888         }
889 
890         /**
891          * Write a comment.
892          *
893          * @param comment the comment to write
894          * @throws IOException if an I/O error occurs.
895          */
896         public void writeComment(final String comment) throws IOException {
897             writeln("# " + comment);
898         }
899 
900         /**
901          * Helper method for writing a line with the platform specific line ending.
902          *
903          * @param s the content of the line (may be <b>null</b>)
904          * @throws IOException if an error occurs
905          * @since 1.3
906          */
907         public void writeln(final String s) throws IOException {
908             if (s != null) {
909                 write(s);
910             }
911             write(getLineSeparator());
912         }
913 
914         /**
915          * Write a property.
916          *
917          * @param key The key of the property
918          * @param values The array of values of the property
919          *
920          * @throws IOException if an I/O error occurs.
921          */
922         public void writeProperty(final String key, final List<?> values) throws IOException {
923             for (final Object value : values) {
924                 writeProperty(key, value);
925             }
926         }
927 
928         /**
929          * Write a property.
930          *
931          * @param key the key of the property
932          * @param value the value of the property
933          *
934          * @throws IOException if an I/O error occurs.
935          */
936         public void writeProperty(final String key, final Object value) throws IOException {
937             writeProperty(key, value, false);
938         }
939 
940         /**
941          * Writes the given property and its value. If the value happens to be a list, the {@code forceSingleLine} flag is
942          * evaluated. If it is set, all values are written on a single line using the list delimiter as separator.
943          *
944          * @param key the property key
945          * @param value the property value
946          * @param forceSingleLine the &quot;force single line&quot; flag
947          * @throws IOException if an error occurs
948          * @since 1.3
949          */
950         public void writeProperty(final String key, final Object value, final boolean forceSingleLine) throws IOException {
951             String v;
952 
953             if (value instanceof List) {
954                 v = null;
955                 final List<?> values = (List<?>) value;
956                 if (forceSingleLine) {
957                     try {
958                         v = String.valueOf(getDelimiterHandler().escapeList(values, valueTransformer));
959                     } catch (final UnsupportedOperationException ignored) {
960                         // the handler may not support escaping lists,
961                         // then the list is written in multiple lines
962                     }
963                 }
964                 if (v == null) {
965                     writeProperty(key, values);
966                     return;
967                 }
968             } else {
969                 v = String.valueOf(getDelimiterHandler().escape(value, valueTransformer));
970             }
971 
972             write(escapeKey(key));
973             write(fetchSeparator(key, value));
974             write(v);
975 
976             writeln(null);
977         }
978     } // class PropertiesWriter
979 
980     /**
981      * Defines default error handling for the special {@code "include"} key by throwing the given exception.
982      *
983      * @since 2.6
984      */
985     public static final ConfigurationConsumer<ConfigurationException> DEFAULT_INCLUDE_LISTENER = e -> {
986         throw e;
987     };
988 
989     /**
990      * Defines error handling as a noop for the special {@code "include"} key.
991      *
992      * @since 2.6
993      */
994     public static final ConfigurationConsumer<ConfigurationException> NOOP_INCLUDE_LISTENER = e -> { /* noop */ };
995 
996     /**
997      * The default encoding (ISO-8859-1 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html)
998      */
999     public static final String DEFAULT_ENCODING = StandardCharsets.ISO_8859_1.name();
1000 
1001     /** Constant for the supported comment characters. */
1002     static final String COMMENT_CHARS = "#!";
1003 
1004     /** Constant for the default properties separator. */
1005     static final String DEFAULT_SEPARATOR = " = ";
1006 
1007     /**
1008      * A string with special characters that need to be unescaped when reading a properties file.
1009      * {@link java.util.Properties} escapes these characters when writing out a properties file.
1010      */
1011     private static final String UNESCAPE_CHARACTERS = ":#=!\\\'\"";
1012 
1013     /**
1014      * This is the name of the property that can point to other properties file for including other properties files.
1015      */
1016     private static String include = "include";
1017 
1018     /**
1019      * This is the name of the property that can point to other properties file for including other properties files.
1020      * <p>
1021      * If the file is absent, processing continues normally.
1022      * </p>
1023      */
1024     private static String includeOptional = "includeoptional";
1025 
1026     /** The list of possible key/value separators */
1027     private static final char[] SEPARATORS = {'=', ':'};
1028 
1029     /** The white space characters used as key/value separators. */
1030     private static final char[] WHITE_SPACE = {' ', '\t', '\f'};
1031 
1032     /** Constant for the platform specific line separator. */
1033     private static final String LINE_SEPARATOR = System.lineSeparator();
1034 
1035     /** Constant for the radix of hex numbers. */
1036     private static final int HEX_RADIX = 16;
1037 
1038     /** Constant for the length of a unicode literal. */
1039     private static final int UNICODE_LEN = 4;
1040 
1041     /** Stores the layout object. */
1042     private PropertiesConfigurationLayout layout;
1043 
1044     /** The include listener for the special {@code "include"} key. */
1045     private ConfigurationConsumer<ConfigurationException> includeListener;
1046 
1047     /** The IOFactory for creating readers and writers. */
1048     private IOFactory ioFactory;
1049 
1050     /** The current {@code FileLocator}. */
1051     private FileLocator locator;
1052 
1053     /** Allow file inclusion or not */
1054     private boolean includesAllowed = true;
1055 
1056     /**
1057      * Creates an empty PropertyConfiguration object which can be used to synthesize a new Properties file by adding values
1058      * and then saving().
1059      */
1060     public PropertiesConfiguration() {
1061         installLayout(createLayout());
1062     }
1063 
1064     /**
1065      * Returns the number of trailing backslashes. This is sometimes needed for the correct handling of escape characters.
1066      *
1067      * @param line the string to investigate
1068      * @return the number of trailing backslashes
1069      */
1070     private static int countTrailingBS(final String line) {
1071         int bsCount = 0;
1072         for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) {
1073             bsCount++;
1074         }
1075 
1076         return bsCount;
1077     }
1078 
1079     /**
1080      * Gets the property value for including other properties files. By default it is "include".
1081      *
1082      * @return A String.
1083      */
1084     public static String getInclude() {
1085         return PropertiesConfiguration.include;
1086     }
1087 
1088     /**
1089      * Gets the property value for including other properties files. By default it is "includeoptional".
1090      * <p>
1091      * If the file is absent, processing continues normally.
1092      * </p>
1093      *
1094      * @return A String.
1095      * @since 2.5
1096      */
1097     public static String getIncludeOptional() {
1098         return PropertiesConfiguration.includeOptional;
1099     }
1100 
1101     /**
1102      * Tests whether a line is a comment, i.e. whether it starts with a comment character.
1103      *
1104      * @param line the line
1105      * @return a flag if this is a comment line
1106      * @since 1.3
1107      */
1108     static boolean isCommentLine(final String line) {
1109         final String s = line.trim();
1110         // blank lines are also treated as comment lines
1111         return s.isEmpty() || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
1112     }
1113 
1114     /**
1115      * Checks whether the specified character needs to be unescaped. This method is called when during reading a property
1116      * file an escape character ('\') is detected. If the character following the escape character is recognized as a
1117      * special character which is escaped per default in a Java properties file, it has to be unescaped.
1118      *
1119      * @param ch the character in question
1120      * @return a flag whether this character has to be unescaped
1121      */
1122     private static boolean needsUnescape(final char ch) {
1123         return UNESCAPE_CHARACTERS.indexOf(ch) >= 0;
1124     }
1125 
1126     /**
1127      * Sets the property value for including other properties files. By default it is "include".
1128      *
1129      * @param inc A String.
1130      */
1131     public static void setInclude(final String inc) {
1132         PropertiesConfiguration.include = inc;
1133     }
1134 
1135     /**
1136      * Sets the property value for including other properties files. By default it is "include".
1137      * <p>
1138      * If the file is absent, processing continues normally.
1139      * </p>
1140      *
1141      * @param inc A String.
1142      * @since 2.5
1143      */
1144     public static void setIncludeOptional(final String inc) {
1145         PropertiesConfiguration.includeOptional = inc;
1146     }
1147 
1148     /**
1149      * <p>
1150      * Unescapes any Java literals found in the {@code String} to a {@code Writer}.
1151      * </p>
1152      * This is a slightly modified version of the StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
1153      * drop escaped separators (i.e '\,').
1154      *
1155      * @param str the {@code String} to unescape, may be null
1156      * @return the processed string
1157      * @throws IllegalArgumentException if the Writer is {@code null}
1158      */
1159     protected static String unescapeJava(final String str) {
1160         return unescapeJava(str, false);
1161     }
1162 
1163     /**
1164      * Unescapes Java literals found in the {@code String} to a {@code Writer}.
1165      * <p>
1166      * When the parameter {@code jupCompatible} is {@code false}, the classic behavior is used (see
1167      * {@link #unescapeJava(String)}). When it's {@code true} a slightly different behavior that's compatible with
1168      * {@link java.util.Properties} is used (see {@link JupIOFactory}).
1169      * </p>
1170      *
1171      * @param str the {@code String} to unescape, may be null
1172      * @param jupCompatible whether unescaping is compatible with {@link java.util.Properties}; otherwise the classic
1173      *        behavior is used
1174      * @return the processed string
1175      * @throws IllegalArgumentException if the Writer is {@code null}
1176      */
1177     protected static String unescapeJava(final String str, final boolean jupCompatible) {
1178         if (str == null) {
1179             return null;
1180         }
1181         final int sz = str.length();
1182         final StringBuilder out = new StringBuilder(sz);
1183         final StringBuilder unicode = new StringBuilder(UNICODE_LEN);
1184         boolean hadSlash = false;
1185         boolean inUnicode = false;
1186         for (int i = 0; i < sz; i++) {
1187             final char ch = str.charAt(i);
1188             if (inUnicode) {
1189                 // if in unicode, then we're reading unicode
1190                 // values in somehow
1191                 unicode.append(ch);
1192                 if (unicode.length() == UNICODE_LEN) {
1193                     // unicode now contains the four hex digits
1194                     // which represents our unicode character
1195                     try {
1196                         final int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
1197                         out.append((char) value);
1198                         unicode.setLength(0);
1199                         inUnicode = false;
1200                         hadSlash = false;
1201                     } catch (final NumberFormatException nfe) {
1202                         throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
1203                     }
1204                 }
1205                 continue;
1206             }
1207 
1208             if (hadSlash) {
1209                 // handle an escaped value
1210                 hadSlash = false;
1211 
1212                 switch (ch) {
1213                 case 'r':
1214                     out.append('\r');
1215                     break;
1216                 case 'f':
1217                     out.append('\f');
1218                     break;
1219                 case 't':
1220                     out.append('\t');
1221                     break;
1222                 case 'n':
1223                     out.append('\n');
1224                     break;
1225                 default:
1226                     if (!jupCompatible && ch == 'b') {
1227                         out.append('\b');
1228                     } else if (ch == 'u') {
1229                         // uh-oh, we're in unicode country....
1230                         inUnicode = true;
1231                     } else {
1232                         // JUP simply throws away the \ of unknown escape sequences
1233                         if (!needsUnescape(ch) && !jupCompatible) {
1234                             out.append('\\');
1235                         }
1236                         out.append(ch);
1237                     }
1238                     break;
1239                 }
1240 
1241                 continue;
1242             }
1243             if (ch == '\\') {
1244                 hadSlash = true;
1245                 continue;
1246             }
1247             out.append(ch);
1248         }
1249 
1250         if (hadSlash) {
1251             // then we're in the weird case of a \ at the end of the
1252             // string, let's output it anyway.
1253             out.append('\\');
1254         }
1255 
1256         return out.toString();
1257     }
1258 
1259     /**
1260      * Creates a copy of this object.
1261      *
1262      * @return the copy
1263      */
1264     @Override
1265     public Object clone() {
1266         final PropertiesConfiguration copy = (PropertiesConfiguration) super.clone();
1267         if (layout != null) {
1268             copy.setLayout(new PropertiesConfigurationLayout(layout));
1269         }
1270         return copy;
1271     }
1272 
1273     /**
1274      * Creates a standard layout object. This configuration is initialized with such a standard layout.
1275      *
1276      * @return the newly created layout object
1277      */
1278     private PropertiesConfigurationLayout createLayout() {
1279         return new PropertiesConfigurationLayout();
1280     }
1281 
1282     /**
1283      * Gets the footer comment. This is a comment at the very end of the file.
1284      *
1285      * @return the footer comment
1286      * @since 2.0
1287      */
1288     public String getFooter() {
1289         beginRead(false);
1290         try {
1291             return getLayout().getFooterComment();
1292         } finally {
1293             endRead();
1294         }
1295     }
1296 
1297     /**
1298      * Gets the comment header.
1299      *
1300      * @return the comment header
1301      * @since 1.1
1302      */
1303     public String getHeader() {
1304         beginRead(false);
1305         try {
1306             return getLayout().getHeaderComment();
1307         } finally {
1308             endRead();
1309         }
1310     }
1311 
1312     /**
1313      * Gets the current include listener, never null.
1314      *
1315      * @return the current include listener, never null.
1316      * @since 2.6
1317      */
1318     public ConfigurationConsumer<ConfigurationException> getIncludeListener() {
1319         return includeListener != null ? includeListener : PropertiesConfiguration.DEFAULT_INCLUDE_LISTENER;
1320     }
1321 
1322     /**
1323      * Gets the {@code IOFactory} to be used for creating readers and writers when loading or saving this configuration.
1324      *
1325      * @return the {@code IOFactory}
1326      * @since 1.7
1327      */
1328     public IOFactory getIOFactory() {
1329         return ioFactory != null ? ioFactory : DefaultIOFactory.INSTANCE;
1330     }
1331 
1332     /**
1333      * Gets the associated layout object.
1334      *
1335      * @return the associated layout object
1336      * @since 1.3
1337      */
1338     public PropertiesConfigurationLayout getLayout() {
1339         return layout;
1340     }
1341 
1342     /**
1343      * Stores the current {@code FileLocator} for a following IO operation. The {@code FileLocator} is needed to resolve
1344      * include files with relative file names.
1345      *
1346      * @param locator the current {@code FileLocator}
1347      * @since 2.0
1348      */
1349     @Override
1350     public void initFileLocator(final FileLocator locator) {
1351         this.locator = locator;
1352     }
1353 
1354     /**
1355      * Installs a layout object. It has to be ensured that the layout is registered as change listener at this
1356      * configuration. If there is already a layout object installed, it has to be removed properly.
1357      *
1358      * @param layout the layout object to be installed
1359      */
1360     private void installLayout(final PropertiesConfigurationLayout layout) {
1361         // only one layout must exist
1362         if (this.layout != null) {
1363             removeEventListener(ConfigurationEvent.ANY, this.layout);
1364         }
1365 
1366         if (layout == null) {
1367             this.layout = createLayout();
1368         } else {
1369             this.layout = layout;
1370         }
1371         addEventListener(ConfigurationEvent.ANY, this.layout);
1372     }
1373 
1374     /**
1375      * Reports the status of file inclusion.
1376      *
1377      * @return True if include files are loaded.
1378      */
1379     public boolean isIncludesAllowed() {
1380         return this.includesAllowed;
1381     }
1382 
1383     /**
1384      * Helper method for loading an included properties file. This method is called by {@code load()} when an
1385      * {@code include} property is encountered. It tries to resolve relative file names based on the current base path. If
1386      * this fails, a resolution based on the location of this properties file is tried.
1387      *
1388      * @param fileName the name of the file to load
1389      * @param optional whether or not the {@code fileName} is optional
1390      * @param seenStack Stack of seen include URLs
1391      * @throws ConfigurationException if loading fails
1392      */
1393     private void loadIncludeFile(final String fileName, final boolean optional, final Deque<URL> seenStack) throws ConfigurationException {
1394         if (locator == null) {
1395             throw new ConfigurationException(
1396                 "Load operation not properly " + "initialized! Do not call read(InputStream) directly," + " but use a FileHandler to load a configuration.");
1397         }
1398 
1399         URL url = locateIncludeFile(locator.getBasePath(), fileName);
1400         if (url == null) {
1401             final URL baseURL = locator.getSourceURL();
1402             if (baseURL != null) {
1403                 url = locateIncludeFile(baseURL.toString(), fileName);
1404             }
1405         }
1406 
1407         if (optional && url == null) {
1408             return;
1409         }
1410 
1411         if (url == null) {
1412             getIncludeListener().accept(new ConfigurationException("Cannot resolve include file " + fileName, new FileNotFoundException(fileName)));
1413         } else {
1414             final FileHandler fh = new FileHandler(this);
1415             fh.setFileLocator(locator);
1416             final FileLocator orgLocator = locator;
1417             try {
1418                 try {
1419                     // Check for cycles
1420                     if (seenStack.contains(url)) {
1421                         throw new ConfigurationException(String.format("Cycle detected loading %s, seen stack: %s", url, seenStack));
1422                     }
1423                     seenStack.add(url);
1424                     try {
1425                         fh.load(url);
1426                     } finally {
1427                         seenStack.pop();
1428                     }
1429                 } catch (final ConfigurationException e) {
1430                     getIncludeListener().accept(e);
1431                 }
1432             } finally {
1433                 locator = orgLocator; // reset locator which is changed by load
1434             }
1435         }
1436     }
1437 
1438     /**
1439      * Tries to obtain the URL of an include file using the specified (optional) base path and file name.
1440      *
1441      * @param basePath the base path
1442      * @param fileName the file name
1443      * @return the URL of the include file or <b>null</b> if it cannot be resolved
1444      */
1445     private URL locateIncludeFile(final String basePath, final String fileName) {
1446         final FileLocator includeLocator = FileLocatorUtils.fileLocator(locator).sourceURL(null).basePath(basePath).fileName(fileName).create();
1447         return FileLocatorUtils.locate(includeLocator);
1448     }
1449 
1450     /**
1451      * This method is invoked by the associated {@link PropertiesConfigurationLayout} object for each property definition
1452      * detected in the parsed properties file. Its task is to check whether this is a special property definition (e.g. the
1453      * {@code include} property). If not, the property must be added to this configuration. The return value indicates
1454      * whether the property should be treated as a normal property. If it is <b>false</b>, the layout object will ignore
1455      * this property.
1456      *
1457      * @param key the property key
1458      * @param value the property value
1459      * @param seenStack the stack of seen include URLs
1460      * @return a flag whether this is a normal property
1461      * @throws ConfigurationException if an error occurs
1462      * @since 1.3
1463      */
1464     boolean propertyLoaded(final String key, final String value, final Deque<URL> seenStack) throws ConfigurationException {
1465         final boolean result;
1466 
1467         if (StringUtils.isNotEmpty(getInclude()) && key.equalsIgnoreCase(getInclude())) {
1468             if (isIncludesAllowed()) {
1469                 final Collection<String> files = getListDelimiterHandler().split(value, true);
1470                 for (final String f : files) {
1471                     loadIncludeFile(interpolate(f), false, seenStack);
1472                 }
1473             }
1474             result = false;
1475         } else if (StringUtils.isNotEmpty(getIncludeOptional()) && key.equalsIgnoreCase(getIncludeOptional())) {
1476             if (isIncludesAllowed()) {
1477                 final Collection<String> files = getListDelimiterHandler().split(value, true);
1478                 for (final String f : files) {
1479                     loadIncludeFile(interpolate(f), true, seenStack);
1480                 }
1481             }
1482             result = false;
1483         } else {
1484             addPropertyInternal(key, value);
1485             result = true;
1486         }
1487 
1488         return result;
1489     }
1490 
1491     /**
1492      * {@inheritDoc} This implementation delegates to the associated layout object which does the actual loading. Note that
1493      * this method does not do any synchronization. This lies in the responsibility of the caller. (Typically, the caller is
1494      * a {@code FileHandler} object which takes care for proper synchronization.)
1495      *
1496      * @since 2.0
1497      */
1498     @Override
1499     public void read(final Reader in) throws ConfigurationException, IOException {
1500         getLayout().load(this, in);
1501     }
1502 
1503     /**
1504      * Sets the footer comment. If set, this comment is written after all properties at the end of the file.
1505      *
1506      * @param footer the footer comment
1507      * @since 2.0
1508      */
1509     public void setFooter(final String footer) {
1510         beginWrite(false);
1511         try {
1512             getLayout().setFooterComment(footer);
1513         } finally {
1514             endWrite();
1515         }
1516     }
1517 
1518     /**
1519      * Sets the comment header.
1520      *
1521      * @param header the header to use
1522      * @since 1.1
1523      */
1524     public void setHeader(final String header) {
1525         beginWrite(false);
1526         try {
1527             getLayout().setHeaderComment(header);
1528         } finally {
1529             endWrite();
1530         }
1531     }
1532 
1533     /**
1534      * Sets the current include listener, may not be null.
1535      *
1536      * @param includeListener the current include listener, may not be null.
1537      * @throws IllegalArgumentException if the {@code includeListener} is null.
1538      * @since 2.6
1539      */
1540     public void setIncludeListener(final ConfigurationConsumer<ConfigurationException> includeListener) {
1541         if (includeListener == null) {
1542             throw new IllegalArgumentException("includeListener must not be null.");
1543         }
1544         this.includeListener = includeListener;
1545     }
1546 
1547     /**
1548      * Controls whether additional files can be loaded by the {@code include = <xxx>} statement or not. This is <b>true</b>
1549      * per default.
1550      *
1551      * @param includesAllowed True if Includes are allowed.
1552      */
1553     public void setIncludesAllowed(final boolean includesAllowed) {
1554         this.includesAllowed = includesAllowed;
1555     }
1556 
1557     /**
1558      * Sets the {@code IOFactory} to be used for creating readers and writers when loading or saving this configuration.
1559      * Using this method a client can customize the reader and writer classes used by the load and save operations. Note
1560      * that this method must be called before invoking one of the {@code load()} and {@code save()} methods. Especially, if
1561      * you want to use a custom {@code IOFactory} for changing the {@code PropertiesReader}, you cannot load the
1562      * configuration data in the constructor.
1563      *
1564      * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>)
1565      * @throws IllegalArgumentException if the {@code IOFactory} is <b>null</b>
1566      * @since 1.7
1567      */
1568     public void setIOFactory(final IOFactory ioFactory) {
1569         if (ioFactory == null) {
1570             throw new IllegalArgumentException("IOFactory must not be null.");
1571         }
1572 
1573         this.ioFactory = ioFactory;
1574     }
1575 
1576     /**
1577      * Sets the associated layout object.
1578      *
1579      * @param layout the new layout object; can be <b>null</b>, then a new layout object will be created
1580      * @since 1.3
1581      */
1582     public void setLayout(final PropertiesConfigurationLayout layout) {
1583         installLayout(layout);
1584     }
1585 
1586     /**
1587      * {@inheritDoc} This implementation delegates to the associated layout object which does the actual saving. Note that,
1588      * analogous to {@link #read(Reader)}, this method does not do any synchronization.
1589      *
1590      * @since 2.0
1591      */
1592     @Override
1593     public void write(final Writer out) throws ConfigurationException, IOException {
1594         getLayout().save(this, out);
1595     }
1596 
1597 }