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