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