    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
    018    package org.apache.commons.configuration;
    019
    020    import java.io.File;
    021    import java.io.FilterWriter;
    022    import java.io.IOException;
    023    import java.io.LineNumberReader;
    024    import java.io.Reader;
    025    import java.io.Writer;
    026    import java.net.URL;
    027    import java.util.ArrayList;
    028    import java.util.Iterator;
    029    import java.util.List;
    030    import java.util.regex.Matcher;
    031    import java.util.regex.Pattern;
    032
    033    import org.apache.commons.lang.ArrayUtils;
    034    import org.apache.commons.lang.StringEscapeUtils;
    035    import org.apache.commons.lang.StringUtils;
    036
    037    /**
    038     * This is the "classic" Properties loader which loads the values from
    039     * a single or multiple files (which can be chained with "include =".
    040     * All given path references are either absolute or relative to the
    041     * file name supplied in the constructor.
    042     * <p>
    043     * In this class, empty PropertyConfigurations can be built, properties
    044     * added and later saved. include statements are (obviously) not supported
    045     * if you don't construct a PropertyConfiguration from a file.
    046     *
    047     * <p>The properties file syntax is explained here, basically it follows
    048     * the syntax of the stream parsed by {@link java.util.Properties#load} and
    049     * adds several useful extensions:
    050     *
    051     * <ul>
    052     *  <li>
    053     *   Each property has the syntax <code>key &lt;separator> value</code>. The
    054     *   separators accepted are {@code '='}, {@code ':'} and any white
    055     *   space character. Examples:
    056     * <pre>
    057     *  key1 = value1
    058     *  key2 : value2
    059     *  key3   value3</pre>
    060     *  </li>
    061     *  <li>
    062     *   The <i>key</i> may use any character, separators must be escaped:
    063     * <pre>
    064     *  key\:foo = bar</pre>
    065     *  </li>
    066     *  <li>
    067     *   <i>value</i> may be separated on different lines if a backslash
    068     *   is placed at the end of the line that continues below.
    069     *  </li>
    070     *  <li>
    071     *   <i>value</i> can contain <em>value delimiters</em> and will then be interpreted
    072     *   as a list of tokens. Default value delimiter is the comma ','. So the
    073     *   following property definition
    074     * <pre>
    075     *  key = This property, has multiple, values
    076     * </pre>
    077     *   will result in a property with three values. You can change the value
    078     *   delimiter using the {@link AbstractConfiguration#setListDelimiter(char)}
    079     *   method. Setting the delimiter to 0 will disable value splitting completely.
    080     *  </li>
    081     *  <li>
    082     *   Commas in each token are escaped placing a backslash right before
    083     *   the comma.
    084     *  </li>
    085     *  <li>
    086     *   If a <i>key</i> is used more than once, the values are appended
    087     *   like if they were on the same line separated with commas. <em>Note</em>:
    088     *   When the configuration file is written back to disk the associated
    089     *   {@link PropertiesConfigurationLayout} object (see below) will
    090     *   try to preserve as much of the original format as possible, i.e. properties
    091     *   with multiple values defined on a single line will also be written back on
    092     *   a single line, and multiple occurrences of a single key will be written on
    093     *   multiple lines. If the {@code addProperty()} method was called
    094     *   multiple times for adding multiple values to a property, these properties
    095     *   will per default be written on multiple lines in the output file, too.
    096     *   Some options of the {@code PropertiesConfigurationLayout} class have
    097     *   influence on that behavior.
    098     *  </li>
    099     *  <li>
    100     *   Blank lines and lines starting with character '#' or '!' are skipped.
    101     *  </li>
    102     *  <li>
    103     *   If a property is named "include" (or whatever is defined by
    104     *   setInclude() and getInclude() and the value of that property is
    105     *   the full path to a file on disk, that file will be included into
    106     *   the configuration. You can also pull in files relative to the parent
    107     *   configuration file. So if you have something like the following:
    108     *
    109     *   include = additional.properties
    110     *
    111     *   Then "additional.properties" is expected to be in the same
    112     *   directory as the parent configuration file.
    113     *
    114     *   The properties in the included file are added to the parent configuration,
    115     *   they do not replace existing properties with the same key.
    116     *
    117     *  </li>
    118     * </ul>
    119     *
    120     * <p>Here is an example of a valid extended properties file:
    121     *
    122     * <p><pre>
    123     *      # lines starting with # are comments
    124     *
    125     *      # This is the simplest property
    126     *      key = value
    127     *
    128     *      # A long property may be separated on multiple lines
    129     *      longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
    130     *                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    131     *
    132     *      # This is a property with many tokens
    133     *      tokens_on_a_line = first token, second token
    134     *
    135     *      # This sequence generates exactly the same result
    136     *      tokens_on_multiple_lines = first token
    137     *      tokens_on_multiple_lines = second token
    138     *
    139     *      # commas may be escaped in tokens
    140     *      commas.escaped = Hi\, what'up?
    141     *
    142     *      # properties can reference other properties
    143     *      base.prop = /base
    144     *      first.prop = ${base.prop}/first
    145     *      second.prop = ${first.prop}/second
    146     * </pre>
    147     *
    148     * <p>A {@code PropertiesConfiguration} object is associated with an
    149     * instance of the {@link PropertiesConfigurationLayout} class,
    150     * which is responsible for storing the layout of the parsed properties file
    151     * (i.e. empty lines, comments, and such things). The {@code getLayout()}
    152     * method can be used to obtain this layout object. With {@code setLayout()}
    153     * a new layout object can be set. This should be done before a properties file
    154     * was loaded.
    155     * <p><em>Note:</em>Configuration objects of this type can be read concurrently
    156     * by multiple threads. However if one of these threads modifies the object,
    157     * synchronization has to be performed manually.
    158     *
    159     * @see java.util.Properties#load
    160     *
    161     * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
    162     * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
    163     * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
    164     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
    165     * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
    166     * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
    167     * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
    168     * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
    169     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
    170     * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
    171     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    172     * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
    173     * @author <a href="mailto:ebourg@apache.org">Emmanuel Bourg</a>
    174     * @version $Id: PropertiesConfiguration.java 1234985 2012-01-23 21:09:09Z oheger $
    175     */
    176    public class PropertiesConfiguration extends AbstractFileConfiguration
    177    {
    178        /** Constant for the supported comment characters.*/
    179        static final String COMMENT_CHARS = "#!";
    180
    181        /** Constant for the default properties separator.*/
    182        static final String DEFAULT_SEPARATOR = " = ";
    183
    184        /**
    185         * Constant for the default {@code IOFactory}. This instance is used
    186         * when no specific factory was set.
    187         */
    188        private static final IOFactory DEFAULT_IO_FACTORY = new DefaultIOFactory();
    189
    190        /**
    191         * This is the name of the property that can point to other
    192         * properties file for including other properties files.
    193         */
    194        private static String include = "include";
    195
    196        /** The list of possible key/value separators */
    197        private static final char[] SEPARATORS = new char[] {'=', ':'};
    198
    199        /** The white space characters used as key/value separators. */
    200        private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'};
    201
    202        /**
    203         * The default encoding (ISO-8859-1 as specified by
    204         * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
    205         */
    206        private static final String DEFAULT_ENCODING = "ISO-8859-1";
    207
    208        /** Constant for the platform specific line separator.*/
    209        private static final String LINE_SEPARATOR = System.getProperty("line.separator");
    210
    211        /** Constant for the escaping character.*/
    212        private static final String ESCAPE = "\\";
    213
    214        /** Constant for the escaped escaping character.*/
    215        private static final String DOUBLE_ESC = ESCAPE + ESCAPE;
    216
    217        /** Constant for the radix of hex numbers.*/
    218        private static final int HEX_RADIX = 16;
    219
    220        /** Constant for the length of a unicode literal.*/
    221        private static final int UNICODE_LEN = 4;
    222
    223        /** Stores the layout object.*/
    224        private PropertiesConfigurationLayout layout;
    225
    226        /** The IOFactory for creating readers and writers.*/
    227        private volatile IOFactory ioFactory;
    228
    229        /** Allow file inclusion or not */
    230        private boolean includesAllowed;
    231
    232        /**
    233         * Creates an empty PropertyConfiguration object which can be
    234         * used to synthesize a new Properties file by adding values and
    235         * then saving().
    236         */
    237        public PropertiesConfiguration()
    238        {
    239            layout = createLayout();
    240            setIncludesAllowed(false);
    241        }
    242
    243        /**
    244         * Creates and loads the extended properties from the specified file.
    245         * The specified file can contain "include = " properties which then
    246         * are loaded and merged into the properties.
    247         *
    248         * @param fileName The name of the properties file to load.
    249         * @throws ConfigurationException Error while loading the properties file
    250         */
    251        public PropertiesConfiguration(String fileName) throws ConfigurationException
    252        {
    253            super(fileName);
    254        }
    255
    256        /**
    257         * Creates and loads the extended properties from the specified file.
    258         * The specified file can contain "include = " properties which then
    259         * are loaded and merged into the properties. If the file does not exist,
    260         * an empty configuration will be created. Later the {@code save()}
    261         * method can be called to save the properties to the specified file.
    262         *
    263         * @param file The properties file to load.
    264         * @throws ConfigurationException Error while loading the properties file
    265         */
    266        public PropertiesConfiguration(File file) throws ConfigurationException
    267        {
    268            super(file);
    269
    270            // If the file does not exist, no layout object was created. We have to
    271            // do this manually in this case.
    272            getLayout();
    273        }
    274
    275        /**
    276         * Creates and loads the extended properties from the specified URL.
    277         * The specified file can contain "include = " properties which then
    278         * are loaded and merged into the properties.
    279         *
    280         * @param url The location of the properties file to load.
    281         * @throws ConfigurationException Error while loading the properties file
    282         */
    283        public PropertiesConfiguration(URL url) throws ConfigurationException
    284        {
    285            super(url);
    286        }
    287
    288        /**
    289         * Gets the property value for including other properties files.
    290         * By default it is "include".
    291         *
    292         * @return A String.
    293         */
    294        public static String getInclude()
    295        {
    296            return PropertiesConfiguration.include;
    297        }
    298
    299        /**
    300         * Sets the property value for including other properties files.
    301         * By default it is "include".
    302         *
    303         * @param inc A String.
    304         */
    305        public static void setInclude(String inc)
    306        {
    307            PropertiesConfiguration.include = inc;
    308        }
    309
    310        /**
    311         * Controls whether additional files can be loaded by the include = <xxx>
    312         * statement or not. Base rule is, that objects created by the empty
    313         * C'tor can not have included files.
    314         *
    315         * @param includesAllowed includesAllowed True if Includes are allowed.
    316         */
    317        protected void setIncludesAllowed(boolean includesAllowed)
    318        {
    319            this.includesAllowed = includesAllowed;
    320        }
    321
    322        /**
    323         * Reports the status of file inclusion.
    324         *
    325         * @return True if include files are loaded.
    326         */
    327        public boolean getIncludesAllowed()
    328        {
    329            return this.includesAllowed;
    330        }
    331
    332        /**
    333         * Return the comment header.
    334         *
    335         * @return the comment header
    336         * @since 1.1
    337         */
    338        public String getHeader()
    339        {
    340            return getLayout().getHeaderComment();
    341        }
    342
    343        /**
    344         * Set the comment header.
    345         *
    346         * @param header the header to use
    347         * @since 1.1
    348         */
    349        public void setHeader(String header)
    350        {
    351            getLayout().setHeaderComment(header);
    352        }
    353
    354        /**
    355         * Returns the encoding to be used when loading or storing configuration
    356         * data. This implementation ensures that the default encoding will be used
    357         * if none has been set explicitly.
    358         *
    359         * @return the encoding
    360         */
    361        @Override
    362        public String getEncoding()
    363        {
    364            String enc = super.getEncoding();
    365            return (enc != null) ? enc : DEFAULT_ENCODING;
    366        }
    367
    368        /**
    369         * Returns the associated layout object.
    370         *
    371         * @return the associated layout object
    372         * @since 1.3
    373         */
    374        public synchronized PropertiesConfigurationLayout getLayout()
    375        {
    376            if (layout == null)
    377            {
    378                layout = createLayout();
    379            }
    380            return layout;
    381        }
    382
    383        /**
    384         * Sets the associated layout object.
    385         *
    386         * @param layout the new layout object; can be <b>null</b>, then a new
    387         * layout object will be created
    388         * @since 1.3
    389         */
    390        public synchronized void setLayout(PropertiesConfigurationLayout layout)
    391        {
    392            // only one layout must exist
    393            if (this.layout != null)
    394            {
    395                removeConfigurationListener(this.layout);
    396            }
    397
    398            if (layout == null)
    399            {
    400                this.layout = createLayout();
    401            }
    402            else
    403            {
    404                this.layout = layout;
    405            }
    406        }
    407
    408        /**
    409         * Creates the associated layout object. This method is invoked when the
    410         * layout object is accessed and has not been created yet. Derived classes
    411         * can override this method to hook in a different layout implementation.
    412         *
    413         * @return the layout object to use
    414         * @since 1.3
    415         */
    416        protected PropertiesConfigurationLayout createLayout()
    417        {
    418            return new PropertiesConfigurationLayout(this);
    419        }
    420
    421        /**
    422         * Returns the {@code IOFactory} to be used for creating readers and
    423         * writers when loading or saving this configuration.
    424         *
    425         * @return the {@code IOFactory}
    426         * @since 1.7
    427         */
    428        public IOFactory getIOFactory()
    429        {
    430            return (ioFactory != null) ? ioFactory : DEFAULT_IO_FACTORY;
    431        }
    432
    433        /**
    434         * Sets the {@code IOFactory} to be used for creating readers and
    435         * writers when loading or saving this configuration. Using this method a
    436         * client can customize the reader and writer classes used by the load and
    437         * save operations. Note that this method must be called before invoking
    438         * one of the {@code load()} and {@code save()} methods.
    439         * Especially, if you want to use a custom {@code IOFactory} for
    440         * changing the {@code PropertiesReader}, you cannot load the
    441         * configuration data in the constructor.
    442         *
    443         * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>)
    444         * @throws IllegalArgumentException if the {@code IOFactory} is
    445         *         <b>null</b>
    446         * @since 1.7
    447         */
    448        public void setIOFactory(IOFactory ioFactory)
    449        {
    450            if (ioFactory == null)
    451            {
    452                throw new IllegalArgumentException("IOFactory must not be null!");
    453            }
    454
    455            this.ioFactory = ioFactory;
    456        }
    457
    458        /**
    459         * Load the properties from the given reader.
    460         * Note that the {@code clear()} method is not called, so
    461         * the properties contained in the loaded file will be added to the
    462         * actual set of properties.
    463         *
    464         * @param in An InputStream.
    465         *
    466         * @throws ConfigurationException if an error occurs
    467         */
    468        public synchronized void load(Reader in) throws ConfigurationException
    469        {
    470            boolean oldAutoSave = isAutoSave();
    471            setAutoSave(false);
    472
    473            try
    474            {
    475                getLayout().load(in);
    476            }
    477            finally
    478            {
    479                setAutoSave(oldAutoSave);
    480            }
    481        }
    482
    483        /**
    484         * Save the configuration to the specified stream.
    485         *
    486         * @param writer the output stream used to save the configuration
    487         * @throws ConfigurationException if an error occurs
    488         */
    489        public void save(Writer writer) throws ConfigurationException
    490        {
    491            enterNoReload();
    492            try
    493            {
    494                getLayout().save(writer);
    495            }
    496            finally
    497            {
    498                exitNoReload();
    499            }
    500        }
    501
    502        /**
    503         * Extend the setBasePath method to turn includes
    504         * on and off based on the existence of a base path.
    505         *
    506         * @param basePath The new basePath to set.
    507         */
    508        @Override
    509        public void setBasePath(String basePath)
    510        {
    511            super.setBasePath(basePath);
    512            setIncludesAllowed(StringUtils.isNotEmpty(basePath));
    513        }
    514
    515        /**
    516         * Creates a copy of this object.
    517         *
    518         * @return the copy
    519         */
    520        @Override
    521        public Object clone()
    522        {
    523            PropertiesConfiguration copy = (PropertiesConfiguration) super.clone();
    524            if (layout != null)
    525            {
    526                copy.setLayout(new PropertiesConfigurationLayout(copy, layout));
    527            }
    528            return copy;
    529        }
    530
    531        /**
    532         * This method is invoked by the associated
    533         * {@link PropertiesConfigurationLayout} object for each
    534         * property definition detected in the parsed properties file. Its task is
    535         * to check whether this is a special property definition (e.g. the
    536         * {@code include} property). If not, the property must be added to
    537         * this configuration. The return value indicates whether the property
    538         * should be treated as a normal property. If it is <b>false</b>, the
    539         * layout object will ignore this property.
    540         *
    541         * @param key the property key
    542         * @param value the property value
    543         * @return a flag whether this is a normal property
    544         * @throws ConfigurationException if an error occurs
    545         * @since 1.3
    546         */
    547        boolean propertyLoaded(String key, String value)
    548                throws ConfigurationException
    549        {
    550            boolean result;
    551
    552            if (StringUtils.isNotEmpty(getInclude())
    553                    && key.equalsIgnoreCase(getInclude()))
    554            {
    555                if (getIncludesAllowed())
    556                {
    557                    String[] files;
    558                    if (!isDelimiterParsingDisabled())
    559                    {
    560                        files = StringUtils.split(value, getListDelimiter());
    561                    }
    562                    else
    563                    {
    564                        files = new String[]{value};
    565                    }
    566                    for (String f : files)
    567                    {
    568                        loadIncludeFile(interpolate(f.trim()));
    569                    }
    570                }
    571                result = false;
    572            }
    573
    574            else
    575            {
    576                addProperty(key, value);
    577                result = true;
    578            }
    579
    580            return result;
    581        }
    582
    583        /**
    584         * Tests whether a line is a comment, i.e. whether it starts with a comment
    585         * character.
    586         *
    587         * @param line the line
    588         * @return a flag if this is a comment line
    589         * @since 1.3
    590         */
    591        static boolean isCommentLine(String line)
    592        {
    593            String s = line.trim();
    594            // blanc lines are also treated as comment lines
    595            return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
    596        }
    597
    598        /**
    599         * Returns the number of trailing backslashes. This is sometimes needed for
    600         * the correct handling of escape characters.
    601         *
    602         * @param line the string to investigate
    603         * @return the number of trailing backslashes
    604         */
    605        private static int countTrailingBS(String line)
    606        {
    607            int bsCount = 0;
    608            for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
    609            {
    610                bsCount++;
    611            }
    612
    613            return bsCount;
    614        }
    615
    616        /**
    617         * This class is used to read properties lines. These lines do
    618         * not terminate with new-line chars but rather when there is no
    619         * backslash sign a the end of the line.  This is used to
    620         * concatenate multiple lines for readability.
    621         */
    622        public static class PropertiesReader extends LineNumberReader
    623        {
    624            /** The regular expression to parse the key and the value of a property. */
    625            private static final Pattern PROPERTY_PATTERN = Pattern
    626                    .compile("(([\\S&&[^\\\\" + new String(SEPARATORS)
    627                            + "]]|\\\\.)*)(\\s*(\\s+|[" + new String(SEPARATORS)
    628                            + "])\\s*)(.*)");
    629
    630            /** Constant for the index of the group for the key. */
    631            private static final int IDX_KEY = 1;
    632
    633            /** Constant for the index of the group for the value. */
    634            private static final int IDX_VALUE = 5;
    635
    636            /** Constant for the index of the group for the separator. */
    637            private static final int IDX_SEPARATOR = 3;
    638
    639            /** Stores the comment lines for the currently processed property.*/
    640            private List<String> commentLines;
    641
    642            /** Stores the name of the last read property.*/
    643            private String propertyName;
    644
    645            /** Stores the value of the last read property.*/
    646            private String propertyValue;
    647
    648            /** Stores the property separator of the last read property.*/
    649            private String propertySeparator = DEFAULT_SEPARATOR;
    650
    651            /** Stores the list delimiter character.*/
    652            private char delimiter;
    653
    654            /**
    655             * Constructor.
    656             *
    657             * @param reader A Reader.
    658             */
    659            public PropertiesReader(Reader reader)
    660            {
    661                this(reader, AbstractConfiguration.getDefaultListDelimiter());
    662            }
    663
    664            /**
    665             * Creates a new instance of {@code PropertiesReader} and sets
    666             * the underlying reader and the list delimiter.
    667             *
    668             * @param reader the reader
    669             * @param listDelimiter the list delimiter character
    670             * @since 1.3
    671             */
    672            public PropertiesReader(Reader reader, char listDelimiter)
    673            {
    674                super(reader);
    675                commentLines = new ArrayList<String>();
    676                delimiter = listDelimiter;
    677            }
    678
    679            /**
    680             * Reads a property line. Returns null if Stream is
    681             * at EOF. Concatenates lines ending with "\".
    682             * Skips lines beginning with "#" or "!" and empty lines.
    683             * The return value is a property definition (<code>&lt;name&gt;</code>
    684             * = <code>&lt;value&gt;</code>)
    685             *
    686             * @return A string containing a property value or null
    687             *
    688             * @throws IOException in case of an I/O error
    689             */
    690            public String readProperty() throws IOException
    691            {
    692                commentLines.clear();
    693                StringBuilder buffer = new StringBuilder();
    694
    695                while (true)
    696                {
    697                    String line = readLine();
    698                    if (line == null)
    699                    {
    700                        // EOF
    701                        return null;
    702                    }
    703
    704                    if (isCommentLine(line))
    705                    {
    706                        commentLines.add(line);
    707                        continue;
    708                    }
    709
    710                    line = line.trim();
    711
    712                    if (checkCombineLines(line))
    713                    {
    714                        line = line.substring(0, line.length() - 1);
    715                        buffer.append(line);
    716                    }
    717                    else
    718                    {
    719                        buffer.append(line);
    720                        break;
    721                    }
    722                }
    723                return buffer.toString();
    724            }
    725
    726            /**
    727             * Parses the next property from the input stream and stores the found
    728             * name and value in internal fields. These fields can be obtained using
    729             * the provided getter methods. The return value indicates whether EOF
    730             * was reached (<b>false</b>) or whether further properties are
    731             * available (<b>true</b>).
    732             *
    733             * @return a flag if further properties are available
    734             * @throws IOException if an error occurs
    735             * @since 1.3
    736             */
    737            public boolean nextProperty() throws IOException
    738            {
    739                String line = readProperty();
    740
    741                if (line == null)
    742                {
    743                    return false; // EOF
    744                }
    745
    746                // parse the line
    747                parseProperty(line);
    748                return true;
    749            }
    750
    751            /**
    752             * Returns the comment lines that have been read for the last property.
    753             *
    754             * @return the comment lines for the last property returned by
    755             * {@code readProperty()}
    756             * @since 1.3
    757             */
    758            public List<String> getCommentLines()
    759            {
    760                return commentLines;
    761            }
    762
    763            /**
    764             * Returns the name of the last read property. This method can be called
    765             * after {@link #nextProperty()} was invoked and its
    766             * return value was <b>true</b>.
    767             *
    768             * @return the name of the last read property
    769             * @since 1.3
    770             */
    771            public String getPropertyName()
    772            {
    773                return propertyName;
    774            }
    775
    776            /**
    777             * Returns the value of the last read property. This method can be
    778             * called after {@link #nextProperty()} was invoked and
    779             * its return value was <b>true</b>.
    780             *
    781             * @return the value of the last read property
    782             * @since 1.3
    783             */
    784            public String getPropertyValue()
    785            {
    786                return propertyValue;
    787            }
    788
    789            /**
    790             * Returns the separator that was used for the last read property. The
    791             * separator can be stored so that it can later be restored when saving
    792             * the configuration.
    793             *
    794             * @return the separator for the last read property
    795             * @since 1.7
    796             */
    797            public String getPropertySeparator()
    798            {
    799                return propertySeparator;
    800            }
    801
    802            /**
    803             * Parses a line read from the properties file. This method is called
    804             * for each non-comment line read from the source file. Its task is to
    805             * split the passed in line into the property key and its value. The
    806             * results of the parse operation can be stored by calling the
    807             * {@code initPropertyXXX()} methods.
    808             *
    809             * @param line the line read from the properties file
    810             * @since 1.7
    811             */
    812            protected void parseProperty(String line)
    813            {
    814                String[] property = doParseProperty(line);
    815                initPropertyName(property[0]);
    816                initPropertyValue(property[1]);
    817                initPropertySeparator(property[2]);
    818            }
    819
    820            /**
    821             * Sets the name of the current property. This method can be called by
    822             * {@code parseProperty()} for storing the results of the parse
    823             * operation. It also ensures that the property key is correctly
    824             * escaped.
    825             *
    826             * @param name the name of the current property
    827             * @since 1.7
    828             */
    829            protected void initPropertyName(String name)
    830            {
    831                propertyName = StringEscapeUtils.unescapeJava(name);
    832            }
    833
    834            /**
    835             * Sets the value of the current property. This method can be called by
    836             * {@code parseProperty()} for storing the results of the parse
    837             * operation. It also ensures that the property value is correctly
    838             * escaped.
    839             *
    840             * @param value the value of the current property
    841             * @since 1.7
    842             */
    843            protected void initPropertyValue(String value)
    844            {
    845                propertyValue = unescapeJava(value, delimiter);
    846            }
    847
    848            /**
    849             * Sets the separator of the current property. This method can be called
    850             * by {@code parseProperty()}. It allows the associated layout
    851             * object to keep track of the property separators. When saving the
    852             * configuration the separators can be restored.
    853             *
    854             * @param value the separator used for the current property
    855             * @since 1.7
    856             */
    857            protected void initPropertySeparator(String value)
    858            {
    859                propertySeparator = value;
    860            }
    861
    862            /**
    863             * Checks if the passed in line should be combined with the following.
    864             * This is true, if the line ends with an odd number of backslashes.
    865             *
    866             * @param line the line
    867             * @return a flag if the lines should be combined
    868             */
    869            private static boolean checkCombineLines(String line)
    870            {
    871                return countTrailingBS(line) % 2 != 0;
    872            }
    873
    874            /**
    875             * Parse a property line and return the key, the value, and the separator in an array.
    876             *
    877             * @param line the line to parse
    878             * @return an array with the property's key, value, and separator
    879             */
    880            private static String[] doParseProperty(String line)
    881            {
    882                Matcher matcher = PROPERTY_PATTERN.matcher(line);
    883
    884                String[] result = {"", "", ""};
    885
    886                if (matcher.matches())
    887                {
    888                    result[0] = matcher.group(IDX_KEY).trim();
    889                    result[1] = matcher.group(IDX_VALUE).trim();
    890                    result[2] = matcher.group(IDX_SEPARATOR);
    891                }
    892
    893                return result;
    894            }
    895        } // class PropertiesReader
    896
    897        /**
    898         * This class is used to write properties lines. The most important method
    899         * is {@code writeProperty(String, Object, boolean)}, which is called
    900         * during a save operation for each property found in the configuration.
    901         */
    902        public static class PropertiesWriter extends FilterWriter
    903        {
    904            /** Constant for the initial size when creating a string buffer. */
    905            private static final int BUF_SIZE = 8;
    906
    907            /** The delimiter for multi-valued properties.*/
    908            private char delimiter;
    909
    910            /** The separator to be used for the current property. */
    911            private String currentSeparator;
    912
    913            /** The global separator. If set, it overrides the current separator.*/
    914            private String globalSeparator;
    915
    916            /** The line separator.*/
    917            private String lineSeparator;
    918
    919            /**
    920             * Constructor.
    921             *
    922             * @param writer a Writer object providing the underlying stream
    923             * @param delimiter the delimiter character for multi-valued properties
    924             */
    925            public PropertiesWriter(Writer writer, char delimiter)
    926            {
    927                super(writer);
    928                this.delimiter = delimiter;
    929            }
    930
    931            /**
    932             * Returns the current property separator.
    933             *
    934             * @return the current property separator
    935             * @since 1.7
    936             */
    937            public String getCurrentSeparator()
    938            {
    939                return currentSeparator;
    940            }
    941
    942            /**
    943             * Sets the current property separator. This separator is used when
    944             * writing the next property.
    945             *
    946             * @param currentSeparator the current property separator
    947             * @since 1.7
    948             */
    949            public void setCurrentSeparator(String currentSeparator)
    950            {
    951                this.currentSeparator = currentSeparator;
    952            }
    953
    954            /**
    955             * Returns the global property separator.
    956             *
    957             * @return the global property separator
    958             * @since 1.7
    959             */
    960            public String getGlobalSeparator()
    961            {
    962                return globalSeparator;
    963            }
    964
    965            /**
    966             * Sets the global property separator. This separator corresponds to the
    967             * {@code globalSeparator} property of
    968             * {@link PropertiesConfigurationLayout}. It defines the separator to be
    969             * used for all properties. If it is undefined, the current separator is
    970             * used.
    971             *
    972             * @param globalSeparator the global property separator
    973             * @since 1.7
    974             */
    975            public void setGlobalSeparator(String globalSeparator)
    976            {
    977                this.globalSeparator = globalSeparator;
    978            }
    979
    980            /**
    981             * Returns the line separator.
    982             *
    983             * @return the line separator
    984             * @since 1.7
    985             */
    986            public String getLineSeparator()
    987            {
    988                return (lineSeparator != null) ? lineSeparator : LINE_SEPARATOR;
    989            }
    990
    991            /**
    992             * Sets the line separator. Each line written by this writer is
    993             * terminated with this separator. If not set, the platform-specific
    994             * line separator is used.
    995             *
    996             * @param lineSeparator the line separator to be used
    997             * @since 1.7
    998             */
    999            public void setLineSeparator(String lineSeparator)
    1000            {
    1001                this.lineSeparator = lineSeparator;
    1002            }
    1003
    1004            /**
    1005             * Write a property.
    1006             *
    1007             * @param key the key of the property
    1008             * @param value the value of the property
    1009             *
    1010             * @throws IOException if an I/O error occurs
    1011             */
    1012            public void writeProperty(String key, Object value) throws IOException
    1013            {
    1014                writeProperty(key, value, false);
    1015            }
    1016
    1017            /**
    1018             * Write a property.
    1019             *
    1020             * @param key The key of the property
    1021             * @param values The array of values of the property
    1022             *
    1023             * @throws IOException if an I/O error occurs
    1024             */
    1025            public void writeProperty(String key, List<?> values) throws IOException
    1026            {
    1027                for (int i = 0; i < values.size(); i++)
    1028                {
    1029                    writeProperty(key, values.get(i));
    1030                }
    1031            }
    1032
    1033            /**
    1034             * Writes the given property and its value. If the value happens to be a
    1035             * list, the {@code forceSingleLine} flag is evaluated. If it is
    1036             * set, all values are written on a single line using the list delimiter
    1037             * as separator.
    1038             *
    1039             * @param key the property key
    1040             * @param value the property value
    1041             * @param forceSingleLine the &quot;force single line&quot; flag
    1042             * @throws IOException if an error occurs
    1043             * @since 1.3
    1044             */
    1045            public void writeProperty(String key, Object value,
    1046                    boolean forceSingleLine) throws IOException
    1047            {
    1048                String v;
    1049
    1050                if (value instanceof List)
    1051                {
    1052                    List<?> values = (List<?>) value;
    1053                    if (forceSingleLine)
    1054                    {
    1055                        v = makeSingleLineValue(values);
    1056                    }
    1057                    else
    1058                    {
    1059                        writeProperty(key, values);
    1060                        return;
    1061                    }
    1062                }
    1063                else
    1064                {
    1065                    v = escapeValue(value, false);
    1066                }
    1067
    1068                write(escapeKey(key));
    1069                write(fetchSeparator(key, value));
    1070                write(v);
    1071
    1072                writeln(null);
    1073            }
    1074
    1075            /**
    1076             * Write a comment.
    1077             *
    1078             * @param comment the comment to write
    1079             * @throws IOException if an I/O error occurs
    1080             */
    1081            public void writeComment(String comment) throws IOException
    1082            {
    1083                writeln("# " + comment);
    1084            }
    1085
    1086            /**
    1087             * Escape the separators in the key.
    1088             *
    1089             * @param key the key
    1090             * @return the escaped key
    1091             * @since 1.2
    1092             */
    1093            private String escapeKey(String key)
    1094            {
    1095                StringBuilder newkey = new StringBuilder();
    1096
    1097                for (int i = 0; i < key.length(); i++)
    1098                {
    1099                    char c = key.charAt(i);
    1100
    1101                    if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c))
    1102                    {
    1103                        // escape the separator
    1104                        newkey.append('\\');
    1105                        newkey.append(c);
    1106                    }
    1107                    else
    1108                    {
    1109                        newkey.append(c);
    1110                    }
    1111                }
    1112
    1113                return newkey.toString();
    1114            }
    1115
    1116            /**
    1117             * Escapes the given property value. Delimiter characters in the value
    1118             * will be escaped.
    1119             *
    1120             * @param value the property value
    1121             * @param inList a flag whether the value is part of a list
    1122             * @return the escaped property value
    1123             * @since 1.3
    1124             */
    1125            private String escapeValue(Object value, boolean inList)
    1126            {
    1127                String escapedValue = handleBackslashs(value, inList);
    1128                if (delimiter != 0)
    1129                {
    1130                    escapedValue = StringUtils.replace(escapedValue, String.valueOf(delimiter), ESCAPE + delimiter);
    1131                }
    1132                return escapedValue;
    1133            }
    1134
    1135            /**
    1136             * Performs the escaping of backslashes in the specified properties
    1137             * value. Because a double backslash is used to escape the escape
    1138             * character of a list delimiter, double backslashes also have to be
    1139             * escaped if the property is part of a (single line) list. Then, in all
    1140             * cases each backslash has to be doubled in order to produce a valid
    1141             * properties file.
    1142             *
    1143             * @param value the value to be escaped
    1144             * @param inList a flag whether the value is part of a list
    1145             * @return the value with escaped backslashes as string
    1146             */
    1147            private String handleBackslashs(Object value, boolean inList)
    1148            {
    1149                String strValue = String.valueOf(value);
    1150
    1151                if (inList && strValue.indexOf(DOUBLE_ESC) >= 0)
    1152                {
    1153                    char esc = ESCAPE.charAt(0);
    1154                    StringBuilder buf = new StringBuilder(strValue.length() + BUF_SIZE);
    1155                    for (int i = 0; i < strValue.length(); i++)
    1156                    {
    1157                        if (strValue.charAt(i) == esc && i < strValue.length() - 1
    1158                                && strValue.charAt(i + 1) == esc)
    1159                        {
    1160                            buf.append(DOUBLE_ESC).append(DOUBLE_ESC);
    1161                            i++;
    1162                        }
    1163                        else
    1164                        {
    1165                            buf.append(strValue.charAt(i));
    1166                        }
    1167                    }
    1168
    1169                    strValue = buf.toString();
    1170                }
    1171
    1172                return StringEscapeUtils.escapeJava(strValue);
    1173            }
    1174
    1175            /**
    1176             * Transforms a list of values into a single line value.
    1177             *
    1178             * @param values the list with the values
    1179             * @return a string with the single line value (can be <b>null</b>)
    1180             * @since 1.3
    1181             */
    1182            private String makeSingleLineValue(List<?> values)
    1183            {
    1184                if (!values.isEmpty())
    1185                {
    1186                    Iterator<?> it = values.iterator();
    1187                    String lastValue = escapeValue(it.next(), true);
    1188                    StringBuilder buf = new StringBuilder(lastValue);
    1189                    while (it.hasNext())
    1190                    {
    1191                        // if the last value ended with an escape character, it has
    1192                        // to be escaped itself; otherwise the list delimiter will
    1193                        // be escaped
    1194                        if (lastValue.endsWith(ESCAPE) && (countTrailingBS(lastValue) / 2) % 2 != 0)
    1195                        {
    1196                            buf.append(ESCAPE).append(ESCAPE);
    1197                        }
    1198                        buf.append(delimiter);
    1199                        lastValue = escapeValue(it.next(), true);
    1200                        buf.append(lastValue);
    1201                    }
    1202                    return buf.toString();
    1203                }
    1204                else
    1205                {
    1206                    return null;
    1207                }
    1208            }
    1209
    1210            /**
    1211             * Helper method for writing a line with the platform specific line
    1212             * ending.
    1213             *
    1214             * @param s the content of the line (may be <b>null</b>)
    1215             * @throws IOException if an error occurs
    1216             * @since 1.3
    1217             */
    1218            public void writeln(String s) throws IOException
    1219            {
    1220                if (s != null)
    1221                {
    1222                    write(s);
    1223                }
    1224                write(getLineSeparator());
    1225            }
    1226
    1227            /**
    1228             * Returns the separator to be used for the given property. This method
    1229             * is called by {@code writeProperty()}. The string returned here
    1230             * is used as separator between the property key and its value. Per
    1231             * default the method checks whether a global separator is set. If this
    1232             * is the case, it is returned. Otherwise the separator returned by
    1233             * {@code getCurrentSeparator()} is used, which was set by the
    1234             * associated layout object. Derived classes may implement a different
    1235             * strategy for defining the separator.
    1236             *
    1237             * @param key the property key
    1238             * @param value the value
    1239             * @return the separator to be used
    1240             * @since 1.7
    1241             */
    1242            protected String fetchSeparator(String key, Object value)
    1243            {
    1244                return (getGlobalSeparator() != null) ? getGlobalSeparator()
    1245                        : getCurrentSeparator();
    1246            }
    1247        } // class PropertiesWriter
    1248
    1249        /**
    1250         * <p>
    1251         * Definition of an interface that allows customization of read and write
    1252         * operations.
    1253         * </p>
    1254         * <p>
    1255         * For reading and writing properties files the inner classes
    1256         * {@code PropertiesReader} and {@code PropertiesWriter} are used.
    1257         * This interface defines factory methods for creating both a
    1258         * {@code PropertiesReader} and a {@code PropertiesWriter}. An
    1259         * object implementing this interface can be passed to the
    1260         * {@code setIOFactory()} method of
    1261         * {@code PropertiesConfiguration}. Every time the configuration is
    1262         * read or written the {@code IOFactory} is asked to create the
    1263         * appropriate reader or writer object. This provides an opportunity to
    1264         * inject custom reader or writer implementations.
    1265         * </p>
    1266         *
    1267         * @since 1.7
    1268         */
    1269        public interface IOFactory
    1270        {
    1271            /**
    1272             * Creates a {@code PropertiesReader} for reading a properties
    1273             * file. This method is called whenever the
    1274             * {@code PropertiesConfiguration} is loaded. The reader returned
    1275             * by this method is then used for parsing the properties file.
    1276             *
    1277             * @param in the underlying reader (of the properties file)
    1278             * @param delimiter the delimiter character for list parsing
    1279             * @return the {@code PropertiesReader} for loading the
    1280             *         configuration
    1281             */
    1282            PropertiesReader createPropertiesReader(Reader in, char delimiter);
    1283
    1284            /**
    1285             * Creates a {@code PropertiesWriter} for writing a properties
    1286             * file. This method is called before the
    1287             * {@code PropertiesConfiguration} is saved. The writer returned by
    1288             * this method is then used for writing the properties file.
    1289             *
    1290             * @param out the underlying writer (to the properties file)
    1291             * @param delimiter the delimiter character for list parsing
    1292             * @return the {@code PropertiesWriter} for saving the
    1293             *         configuration
    1294             */
    1295            PropertiesWriter createPropertiesWriter(Writer out, char delimiter);
    1296        }
    1297
    1298        /**
    1299         * <p>
    1300         * A default implementation of the {@code IOFactory} interface.
    1301         * </p>
    1302         * <p>
    1303         * This class implements the {@code createXXXX()} methods defined by
    1304         * the {@code IOFactory} interface in a way that the default objects
    1305         * (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are
    1306         * returned. Customizing either the reader or the writer (or both) can be
    1307         * done by extending this class and overriding the corresponding
    1308         * {@code createXXXX()} method.
    1309         * </p>
    1310         *
    1311         * @since 1.7
    1312         */
    1313        public static class DefaultIOFactory implements IOFactory
    1314        {
    1315            public PropertiesReader createPropertiesReader(Reader in, char delimiter)
    1316            {
    1317                return new PropertiesReader(in, delimiter);
    1318            }
    1319
    1320            public PropertiesWriter createPropertiesWriter(Writer out,
    1321                    char delimiter)
    1322            {
    1323                return new PropertiesWriter(out, delimiter);
    1324            }
    1325        }
    1326
    1327        /**
    1328         * <p>Unescapes any Java literals found in the {@code String} to a
    1329         * {@code Writer}.</p> This is a slightly modified version of the
    1330         * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
    1331         * drop escaped separators (i.e '\,').
    1332         *
    1333         * @param str  the {@code String} to unescape, may be null
    1334         * @param delimiter the delimiter for multi-valued properties
    1335         * @return the processed string
    1336         * @throws IllegalArgumentException if the Writer is {@code null}
    1337         */
    1338        protected static String unescapeJava(String str, char delimiter)
    1339        {
    1340            if (str == null)
    1341            {
    1342                return null;
    1343            }
    1344            int sz = str.length();
    1345            StringBuilder out = new StringBuilder(sz);
    1346            StringBuilder unicode = new StringBuilder(UNICODE_LEN);
    1347            boolean hadSlash = false;
    1348            boolean inUnicode = false;
    1349            for (int i = 0; i < sz; i++)
    1350            {
    1351                char ch = str.charAt(i);
    1352                if (inUnicode)
    1353                {
    1354                    // if in unicode, then we're reading unicode
    1355                    // values in somehow
    1356                    unicode.append(ch);
    1357                    if (unicode.length() == UNICODE_LEN)
    1358                    {
    1359                        // unicode now contains the four hex digits
    1360                        // which represents our unicode character
    1361                        try
    1362                        {
    1363                            int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
    1364                            out.append((char) value);
    1365                            unicode.setLength(0);
    1366                            inUnicode = false;
    1367                            hadSlash = false;
    1368                        }
    1369                        catch (NumberFormatException nfe)
    1370                        {
    1371                            throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
    1372                        }
    1373                    }
    1374                    continue;
    1375                }
    1376
    1377                if (hadSlash)
    1378                {
    1379                    // handle an escaped value
    1380                    hadSlash = false;
    1381
    1382                    if (ch == '\\')
    1383                    {
    1384                        out.append('\\');
    1385                    }
    1386                    else if (ch == '\'')
    1387                    {
    1388                        out.append('\'');
    1389                    }
    1390                    else if (ch == '\"')
    1391                    {
    1392                        out.append('"');
    1393                    }
    1394                    else if (ch == 'r')
    1395                    {
    1396                        out.append('\r');
    1397                    }
    1398                    else if (ch == 'f')
    1399                    {
    1400                        out.append('\f');
    1401                    }
    1402                    else if (ch == 't')
    1403                    {
    1404                        out.append('\t');
    1405                    }
    1406                    else if (ch == 'n')
    1407                    {
    1408                        out.append('\n');
    1409                    }
    1410                    else if (ch == 'b')
    1411                    {
    1412                        out.append('\b');
    1413                    }
    1414                    else if (ch == delimiter)
    1415                    {
    1416                        out.append('\\');
    1417                        out.append(delimiter);
    1418                    }
    1419                    else if (ch == 'u')
    1420                    {
    1421                        // uh-oh, we're in unicode country....
    1422                        inUnicode = true;
    1423                    }
    1424                    else
    1425                    {
    1426                        out.append(ch);
    1427                    }
    1428
    1429                    continue;
    1430                }
    1431                else if (ch == '\\')
    1432                {
    1433                    hadSlash = true;
    1434                    continue;
    1435                }
    1436                out.append(ch);
    1437            }
    1438
    1439            if (hadSlash)
    1440            {
    1441                // then we're in the weird case of a \ at the end of the
    1442                // string, let's output it anyway.
    1443                out.append('\\');
    1444            }
    1445
    1446            return out.toString();
    1447        }
    1448
    1449        /**
    1450         * Helper method for loading an included properties file. This method is
    1451         * called by {@code load()} when an {@code include} property
    1452         * is encountered. It tries to resolve relative file names based on the
    1453         * current base path. If this fails, a resolution based on the location of
    1454         * this properties file is tried.
    1455         *
    1456         * @param fileName the name of the file to load
    1457         * @throws ConfigurationException if loading fails
    1458         */
    1459        private void loadIncludeFile(String fileName) throws ConfigurationException
    1460        {
    1461            URL url = ConfigurationUtils.locate(getFileSystem(), getBasePath(), fileName);
    1462            if (url == null)
    1463            {
    1464                URL baseURL = getURL();
    1465                if (baseURL != null)
    1466                {
    1467                    url = ConfigurationUtils.locate(getFileSystem(), baseURL.toString(), fileName);
    1468                }
    1469            }
    1470
    1471            if (url == null)
    1472            {
    1473                throw new ConfigurationException("Cannot resolve include file "
    1474                        + fileName);
    1475            }
    1476            load(url);
    1477        }
    1478    }