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