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