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