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