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