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    }