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