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