    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        /**
    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 <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    /**
    1042     * Returns the number of trailing backslashes. This is sometimes needed for the correct handling of escape characters.
    1043     *
    1044     * @param line the string to investigate
    1045     * @return the number of trailing backslashes
    1046     */
    1047    private static int countTrailingBS(final String line) {
    1048        int bsCount = 0;
    1049        for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) {
    1050            bsCount++;
    1051        }
    1052
    1053        return bsCount;
    1054    }
    1055
    1056    /**
    1057     * Gets the property value for including other properties files. By default it is "include".
    1058     *
    1059     * @return A String.
    1060     */
    1061    public static String getInclude() {
    1062        return include;
    1063    }
    1064
    1065    /**
    1066     * Gets the property value for including other properties files. By default it is "includeoptional".
    1067     * <p>
    1068     * If the file is absent, processing continues normally.
    1069     * </p>
    1070     *
    1071     * @return A String.
    1072     * @since 2.5
    1073     */
    1074    public static String getIncludeOptional() {
    1075        return includeOptional;
    1076    }
    1077
    1078    /**
    1079     * Tests whether a line is a comment, i.e. whether it starts with a comment character.
    1080     *
    1081     * @param line the line
    1082     * @return a flag if this is a comment line
    1083     * @since 1.3
    1084     */
    1085    static boolean isCommentLine(final String line) {
    1086        final String s = line.trim();
    1087        // blank lines are also treated as comment lines
    1088        return s.isEmpty() || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
    1089    }
    1090
    1091    /**
    1092     * Checks whether the specified character needs to be unescaped. This method is called when during reading a property
    1093     * file an escape character ('\') is detected. If the character following the escape character is recognized as a
    1094     * special character which is escaped per default in a Java properties file, it has to be unescaped.
    1095     *
    1096     * @param ch the character in question
    1097     * @return a flag whether this character has to be unescaped
    1098     */
    1099    private static boolean needsUnescape(final char ch) {
    1100        return UNESCAPE_CHARACTERS.indexOf(ch) >= 0;
    1101    }
    1102
    1103    /**
    1104     * Sets the property value for including other properties files. By default it is "include".
    1105     *
    1106     * @param inc A String.
    1107     */
    1108    public static void setInclude(final String inc) {
    1109        include = inc;
    1110    }
    1111
    1112    /**
    1113     * Sets the property value for including other properties files. By default it is "include".
    1114     * <p>
    1115     * If the file is absent, processing continues normally.
    1116     * </p>
    1117     *
    1118     * @param inc A String.
    1119     * @since 2.5
    1120     */
    1121    public static void setIncludeOptional(final String inc) {
    1122        includeOptional = inc;
    1123    }
    1124
    1125    /**
    1126     * <p>
    1127     * Unescapes any Java literals found in the {@code String} to a {@code Writer}.
    1128     * </p>
    1129     * This is a slightly modified version of the StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
    1130     * drop escaped separators (i.e '\,').
    1131     *
    1132     * @param str the {@code String} to unescape, may be null
    1133     * @return the processed string
    1134     * @throws IllegalArgumentException if the Writer is {@code null}
    1135     */
    1136    protected static String unescapeJava(final String str) {
    1137        return unescapeJava(str, false);
    1138    }
    1139
    1140    /**
    1141     * Unescapes Java literals found in the {@code String} to a {@code Writer}.
    1142     * <p>
    1143     * When the parameter {@code jupCompatible} is {@code false}, the classic behavior is used (see
    1144     * {@link #unescapeJava(String)}). When it's {@code true} a slightly different behavior that's compatible with
    1145     * {@link java.util.Properties} is used (see {@link JupIOFactory}).
    1146     * </p>
    1147     *
    1148     * @param str the {@code String} to unescape, may be null
    1149     * @param jupCompatible whether unescaping is compatible with {@link java.util.Properties}; otherwise the classic
    1150     *        behavior is used
    1151     * @return the processed string
    1152     * @throws IllegalArgumentException if the Writer is {@code null}
    1153     */
    1154    protected static String unescapeJava(final String str, final boolean jupCompatible) {
    1155        if (str == null) {
    1156            return null;
    1157        }
    1158        final int sz = str.length();
    1159        final StringBuilder out = new StringBuilder(sz);
    1160        final StringBuilder unicode = new StringBuilder(UNICODE_LEN);
    1161        boolean hadSlash = false;
    1162        boolean inUnicode = false;
    1163        for (int i = 0; i < sz; i++) {
    1164            final char ch = str.charAt(i);
    1165            if (inUnicode) {
    1166                // if in unicode, then we're reading unicode
    1167                // values in somehow
    1168                unicode.append(ch);
    1169                if (unicode.length() == UNICODE_LEN) {
    1170                    // unicode now contains the four hex digits
    1171                    // which represents our unicode character
    1172                    try {
    1173                        final int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
    1174                        out.append((char) value);
    1175                        unicode.setLength(0);
    1176                        inUnicode = false;
    1177                        hadSlash = false;
    1178                    } catch (final NumberFormatException nfe) {
    1179                        throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
    1180                    }
    1181                }
    1182                continue;
    1183            }
    1184
    1185            if (hadSlash) {
    1186                // handle an escaped value
    1187                hadSlash = false;
    1188
    1189                switch (ch) {
    1190                case 'r':
    1191                    out.append('\r');
    1192                    break;
    1193                case 'f':
    1194                    out.append('\f');
    1195                    break;
    1196                case 't':
    1197                    out.append('\t');
    1198                    break;
    1199                case 'n':
    1200                    out.append('\n');
    1201                    break;
    1202                default:
    1203                    if (!jupCompatible && ch == 'b') {
    1204                        out.append('\b');
    1205                    } else if (ch == 'u') {
    1206                        // uh-oh, we're in unicode country....
    1207                        inUnicode = true;
    1208                    } else {
    1209                        // JUP simply throws away the \ of unknown escape sequences
    1210                        if (!needsUnescape(ch) && !jupCompatible) {
    1211                            out.append('\\');
    1212                        }
    1213                        out.append(ch);
    1214                    }
    1215                    break;
    1216                }
    1217
    1218                continue;
    1219            }
    1220            if (ch == '\\') {
    1221                hadSlash = true;
    1222                continue;
    1223            }
    1224            out.append(ch);
    1225        }
    1226
    1227        if (hadSlash) {
    1228            // then we're in the weird case of a \ at the end of the
    1229            // string, let's output it anyway.
    1230            out.append('\\');
    1231        }
    1232
    1233        return out.toString();
    1234    }
    1235
    1236    /** Stores the layout object. */
    1237    private PropertiesConfigurationLayout layout;
    1238
    1239    /** The include listener for the special {@code "include"} key. */
    1240    private ConfigurationConsumer<ConfigurationException> includeListener;
    1241
    1242    /** The IOFactory for creating readers and writers. */
    1243    private IOFactory ioFactory;
    1244
    1245    /** The current {@code FileLocator}. */
    1246    private FileLocator locator;
    1247
    1248    /** Allow file inclusion or not */
    1249    private boolean includesAllowed = true;
    1250
    1251    /**
    1252     * Creates an empty PropertyConfiguration object which can be used to synthesize a new Properties file by adding values
    1253     * and then saving().
    1254     */
    1255    public PropertiesConfiguration() {
    1256        installLayout(createLayout());
    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 : 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}