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 727168 2008-12-16 21:44:29Z oheger $
174     */
175    public class PropertiesConfiguration extends AbstractFileConfiguration
176    {
177        /** Constant for the supported comment characters.*/
178        static final String COMMENT_CHARS = "#!";
179    
180        /**
181         * This is the name of the property that can point to other
182         * properties file for including other properties files.
183         */
184        private static String include = "include";
185    
186        /** The list of possible key/value separators */
187        private static final char[] SEPARATORS = new char[] {'=', ':'};
188    
189        /** The white space characters used as key/value separators. */
190        private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'};
191    
192        /**
193         * The default encoding (ISO-8859-1 as specified by
194         * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
195         */
196        private static final String DEFAULT_ENCODING = "ISO-8859-1";
197    
198        /** Constant for the platform specific line separator.*/
199        private static final String LINE_SEPARATOR = System.getProperty("line.separator");
200    
201        /** Constant for the escaping character.*/
202        private static final String ESCAPE = "\\";
203    
204        /** Constant for the radix of hex numbers.*/
205        private static final int HEX_RADIX = 16;
206    
207        /** Constant for the length of a unicode literal.*/
208        private static final int UNICODE_LEN = 4;
209    
210        /** Stores the layout object.*/
211        private PropertiesConfigurationLayout layout;
212    
213        /** Allow file inclusion or not */
214        private boolean includesAllowed;
215    
216        /**
217         * Creates an empty PropertyConfiguration object which can be
218         * used to synthesize a new Properties file by adding values and
219         * then saving().
220         */
221        public PropertiesConfiguration()
222        {
223            layout = createLayout();
224            setIncludesAllowed(false);
225        }
226    
227        /**
228         * Creates and loads the extended properties from the specified file.
229         * The specified file can contain "include = " properties which then
230         * are loaded and merged into the properties.
231         *
232         * @param fileName The name of the properties file to load.
233         * @throws ConfigurationException Error while loading the properties file
234         */
235        public PropertiesConfiguration(String fileName) throws ConfigurationException
236        {
237            super(fileName);
238        }
239    
240        /**
241         * Creates and loads the extended properties from the specified file.
242         * The specified file can contain "include = " properties which then
243         * are loaded and merged into the properties. If the file does not exist,
244         * an empty configuration will be created. Later the <code>save()</code>
245         * method can be called to save the properties to the specified file.
246         *
247         * @param file The properties file to load.
248         * @throws ConfigurationException Error while loading the properties file
249         */
250        public PropertiesConfiguration(File file) throws ConfigurationException
251        {
252            super(file);
253    
254            // If the file does not exist, no layout object was created. We have to
255            // do this manually in this case.
256            getLayout();
257        }
258    
259        /**
260         * Creates and loads the extended properties from the specified URL.
261         * The specified file can contain "include = " properties which then
262         * are loaded and merged into the properties.
263         *
264         * @param url The location of the properties file to load.
265         * @throws ConfigurationException Error while loading the properties file
266         */
267        public PropertiesConfiguration(URL url) throws ConfigurationException
268        {
269            super(url);
270        }
271    
272        /**
273         * Gets the property value for including other properties files.
274         * By default it is "include".
275         *
276         * @return A String.
277         */
278        public static String getInclude()
279        {
280            return PropertiesConfiguration.include;
281        }
282    
283        /**
284         * Sets the property value for including other properties files.
285         * By default it is "include".
286         *
287         * @param inc A String.
288         */
289        public static void setInclude(String inc)
290        {
291            PropertiesConfiguration.include = inc;
292        }
293    
294        /**
295         * Controls whether additional files can be loaded by the include = <xxx>
296         * statement or not. Base rule is, that objects created by the empty
297         * C'tor can not have included files.
298         *
299         * @param includesAllowed includesAllowed True if Includes are allowed.
300         */
301        protected void setIncludesAllowed(boolean includesAllowed)
302        {
303            this.includesAllowed = includesAllowed;
304        }
305    
306        /**
307         * Reports the status of file inclusion.
308         *
309         * @return True if include files are loaded.
310         */
311        public boolean getIncludesAllowed()
312        {
313            return this.includesAllowed;
314        }
315    
316        /**
317         * Return the comment header.
318         *
319         * @return the comment header
320         * @since 1.1
321         */
322        public String getHeader()
323        {
324            return getLayout().getHeaderComment();
325        }
326    
327        /**
328         * Set the comment header.
329         *
330         * @param header the header to use
331         * @since 1.1
332         */
333        public void setHeader(String header)
334        {
335            getLayout().setHeaderComment(header);
336        }
337    
338        /**
339         * Returns the encoding to be used when loading or storing configuration
340         * data. This implementation ensures that the default encoding will be used
341         * if none has been set explicitly.
342         *
343         * @return the encoding
344         */
345        public String getEncoding()
346        {
347            String enc = super.getEncoding();
348            return (enc != null) ? enc : DEFAULT_ENCODING;
349        }
350    
351        /**
352         * Returns the associated layout object.
353         *
354         * @return the associated layout object
355         * @since 1.3
356         */
357        public synchronized PropertiesConfigurationLayout getLayout()
358        {
359            if (layout == null)
360            {
361                layout = createLayout();
362            }
363            return layout;
364        }
365    
366        /**
367         * Sets the associated layout object.
368         *
369         * @param layout the new layout object; can be <b>null</b>, then a new
370         * layout object will be created
371         * @since 1.3
372         */
373        public synchronized void setLayout(PropertiesConfigurationLayout layout)
374        {
375            // only one layout must exist
376            if (this.layout != null)
377            {
378                removeConfigurationListener(this.layout);
379            }
380    
381            if (layout == null)
382            {
383                this.layout = createLayout();
384            }
385            else
386            {
387                this.layout = layout;
388            }
389        }
390    
391        /**
392         * Creates the associated layout object. This method is invoked when the
393         * layout object is accessed and has not been created yet. Derived classes
394         * can override this method to hook in a different layout implementation.
395         *
396         * @return the layout object to use
397         * @since 1.3
398         */
399        protected PropertiesConfigurationLayout createLayout()
400        {
401            return new PropertiesConfigurationLayout(this);
402        }
403    
404        /**
405         * Load the properties from the given reader.
406         * Note that the <code>clear()</code> method is not called, so
407         * the properties contained in the loaded file will be added to the
408         * actual set of properties.
409         *
410         * @param in An InputStream.
411         *
412         * @throws ConfigurationException if an error occurs
413         */
414        public synchronized void load(Reader in) throws ConfigurationException
415        {
416            boolean oldAutoSave = isAutoSave();
417            setAutoSave(false);
418    
419            try
420            {
421                getLayout().load(in);
422            }
423            finally
424            {
425                setAutoSave(oldAutoSave);
426            }
427        }
428    
429        /**
430         * Save the configuration to the specified stream.
431         *
432         * @param writer the output stream used to save the configuration
433         * @throws ConfigurationException if an error occurs
434         */
435        public void save(Writer writer) throws ConfigurationException
436        {
437            enterNoReload();
438            try
439            {
440                getLayout().save(writer);
441            }
442            finally
443            {
444                exitNoReload();
445            }
446        }
447    
448        /**
449         * Extend the setBasePath method to turn includes
450         * on and off based on the existence of a base path.
451         *
452         * @param basePath The new basePath to set.
453         */
454        public void setBasePath(String basePath)
455        {
456            super.setBasePath(basePath);
457            setIncludesAllowed(StringUtils.isNotEmpty(basePath));
458        }
459    
460        /**
461         * Creates a copy of this object.
462         *
463         * @return the copy
464         */
465        public Object clone()
466        {
467            PropertiesConfiguration copy = (PropertiesConfiguration) super.clone();
468            if (layout != null)
469            {
470                copy.setLayout(new PropertiesConfigurationLayout(copy, layout));
471            }
472            return copy;
473        }
474    
475        /**
476         * This method is invoked by the associated
477         * <code>{@link PropertiesConfigurationLayout}</code> object for each
478         * property definition detected in the parsed properties file. Its task is
479         * to check whether this is a special property definition (e.g. the
480         * <code>include</code> property). If not, the property must be added to
481         * this configuration. The return value indicates whether the property
482         * should be treated as a normal property. If it is <b>false</b>, the
483         * layout object will ignore this property.
484         *
485         * @param key the property key
486         * @param value the property value
487         * @return a flag whether this is a normal property
488         * @throws ConfigurationException if an error occurs
489         * @since 1.3
490         */
491        boolean propertyLoaded(String key, String value)
492                throws ConfigurationException
493        {
494            boolean result;
495    
496            if (StringUtils.isNotEmpty(getInclude())
497                    && key.equalsIgnoreCase(getInclude()))
498            {
499                if (getIncludesAllowed())
500                {
501                    String[] files;
502                    if (!isDelimiterParsingDisabled())
503                    {
504                        files = StringUtils.split(value, getListDelimiter());
505                    }
506                    else
507                    {
508                        files = new String[]{value};
509                    }
510                    for (int i = 0; i < files.length; i++)
511                    {
512                        loadIncludeFile(interpolate(files[i].trim()));
513                    }
514                }
515                result = false;
516            }
517    
518            else
519            {
520                addProperty(key, value);
521                result = true;
522            }
523    
524            return result;
525        }
526    
527        /**
528         * Tests whether a line is a comment, i.e. whether it starts with a comment
529         * character.
530         *
531         * @param line the line
532         * @return a flag if this is a comment line
533         * @since 1.3
534         */
535        static boolean isCommentLine(String line)
536        {
537            String s = line.trim();
538            // blanc lines are also treated as comment lines
539            return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
540        }
541    
542        /**
543         * This class is used to read properties lines. These lines do
544         * not terminate with new-line chars but rather when there is no
545         * backslash sign a the end of the line.  This is used to
546         * concatenate multiple lines for readability.
547         */
548        public static class PropertiesReader extends LineNumberReader
549        {
550            /** Stores the comment lines for the currently processed property.*/
551            private List commentLines;
552    
553            /** Stores the name of the last read property.*/
554            private String propertyName;
555    
556            /** Stores the value of the last read property.*/
557            private String propertyValue;
558    
559            /** Stores the list delimiter character.*/
560            private char delimiter;
561    
562            /**
563             * Constructor.
564             *
565             * @param reader A Reader.
566             */
567            public PropertiesReader(Reader reader)
568            {
569                this(reader, AbstractConfiguration.getDefaultListDelimiter());
570            }
571    
572            /**
573             * Creates a new instance of <code>PropertiesReader</code> and sets
574             * the underlaying reader and the list delimiter.
575             *
576             * @param reader the reader
577             * @param listDelimiter the list delimiter character
578             * @since 1.3
579             */
580            public PropertiesReader(Reader reader, char listDelimiter)
581            {
582                super(reader);
583                commentLines = new ArrayList();
584                delimiter = listDelimiter;
585            }
586    
587            /**
588             * Reads a property line. Returns null if Stream is
589             * at EOF. Concatenates lines ending with "\".
590             * Skips lines beginning with "#" or "!" and empty lines.
591             * The return value is a property definition (<code>&lt;name&gt;</code>
592             * = <code>&lt;value&gt;</code>)
593             *
594             * @return A string containing a property value or null
595             *
596             * @throws IOException in case of an I/O error
597             */
598            public String readProperty() throws IOException
599            {
600                commentLines.clear();
601                StringBuffer buffer = new StringBuffer();
602    
603                while (true)
604                {
605                    String line = readLine();
606                    if (line == null)
607                    {
608                        // EOF
609                        return null;
610                    }
611    
612                    if (isCommentLine(line))
613                    {
614                        commentLines.add(line);
615                        continue;
616                    }
617    
618                    line = line.trim();
619    
620                    if (checkCombineLines(line))
621                    {
622                        line = line.substring(0, line.length() - 1);
623                        buffer.append(line);
624                    }
625                    else
626                    {
627                        buffer.append(line);
628                        break;
629                    }
630                }
631                return buffer.toString();
632            }
633    
634            /**
635             * Parses the next property from the input stream and stores the found
636             * name and value in internal fields. These fields can be obtained using
637             * the provided getter methods. The return value indicates whether EOF
638             * was reached (<b>false</b>) or whether further properties are
639             * available (<b>true</b>).
640             *
641             * @return a flag if further properties are available
642             * @throws IOException if an error occurs
643             * @since 1.3
644             */
645            public boolean nextProperty() throws IOException
646            {
647                String line = readProperty();
648    
649                if (line == null)
650                {
651                    return false; // EOF
652                }
653    
654                // parse the line
655                String[] property = parseProperty(line);
656                propertyName = StringEscapeUtils.unescapeJava(property[0]);
657                propertyValue = unescapeJava(property[1], delimiter);
658                return true;
659            }
660    
661            /**
662             * Returns the comment lines that have been read for the last property.
663             *
664             * @return the comment lines for the last property returned by
665             * <code>readProperty()</code>
666             * @since 1.3
667             */
668            public List getCommentLines()
669            {
670                return commentLines;
671            }
672    
673            /**
674             * Returns the name of the last read property. This method can be called
675             * after <code>{@link #nextProperty()}</code> was invoked and its
676             * return value was <b>true</b>.
677             *
678             * @return the name of the last read property
679             * @since 1.3
680             */
681            public String getPropertyName()
682            {
683                return propertyName;
684            }
685    
686            /**
687             * Returns the value of the last read property. This method can be
688             * called after <code>{@link #nextProperty()}</code> was invoked and
689             * its return value was <b>true</b>.
690             *
691             * @return the value of the last read property
692             * @since 1.3
693             */
694            public String getPropertyValue()
695            {
696                return propertyValue;
697            }
698    
699            /**
700             * Checks if the passed in line should be combined with the following.
701             * This is true, if the line ends with an odd number of backslashes.
702             *
703             * @param line the line
704             * @return a flag if the lines should be combined
705             */
706            private static boolean checkCombineLines(String line)
707            {
708                int bsCount = 0;
709                for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
710                {
711                    bsCount++;
712                }
713    
714                return bsCount % 2 != 0;
715            }
716    
717            /**
718             * Parse a property line and return the key and the value in an array.
719             *
720             * @param line the line to parse
721             * @return an array with the property's key and value
722             * @since 1.2
723             */
724            private static String[] parseProperty(String line)
725            {
726                // sorry for this spaghetti code, please replace it as soon as
727                // possible with a regexp when the Java 1.3 requirement is dropped
728    
729                String[] result = new String[2];
730                StringBuffer key = new StringBuffer();
731                StringBuffer value = new StringBuffer();
732    
733                // state of the automaton:
734                // 0: key parsing
735                // 1: antislash found while parsing the key
736                // 2: separator crossing
737                // 3: value parsing
738                int state = 0;
739    
740                for (int pos = 0; pos < line.length(); pos++)
741                {
742                    char c = line.charAt(pos);
743    
744                    switch (state)
745                    {
746                        case 0:
747                            if (c == '\\')
748                            {
749                                state = 1;
750                            }
751                            else if (ArrayUtils.contains(WHITE_SPACE, c))
752                            {
753                                // switch to the separator crossing state
754                                state = 2;
755                            }
756                            else if (ArrayUtils.contains(SEPARATORS, c))
757                            {
758                                // switch to the value parsing state
759                                state = 3;
760                            }
761                            else
762                            {
763                                key.append(c);
764                            }
765    
766                            break;
767    
768                        case 1:
769                            if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c))
770                            {
771                                // this is an escaped separator or white space
772                                key.append(c);
773                            }
774                            else
775                            {
776                                // another escaped character, the '\' is preserved
777                                key.append('\\');
778                                key.append(c);
779                            }
780    
781                            // return to the key parsing state
782                            state = 0;
783    
784                            break;
785    
786                        case 2:
787                            if (ArrayUtils.contains(WHITE_SPACE, c))
788                            {
789                                // do nothing, eat all white spaces
790                                state = 2;
791                            }
792                            else if (ArrayUtils.contains(SEPARATORS, c))
793                            {
794                                // switch to the value parsing state
795                                state = 3;
796                            }
797                            else
798                            {
799                                // any other character indicates we encoutered the beginning of the value
800                                value.append(c);
801    
802                                // switch to the value parsing state
803                                state = 3;
804                            }
805    
806                            break;
807    
808                        case 3:
809                            value.append(c);
810                            break;
811                    }
812                }
813    
814                result[0] = key.toString().trim();
815                result[1] = value.toString().trim();
816    
817                return result;
818            }
819        } // class PropertiesReader
820    
821        /**
822         * This class is used to write properties lines.
823         */
824        public static class PropertiesWriter extends FilterWriter
825        {
826            /** The delimiter for multi-valued properties.*/
827            private char delimiter;
828    
829            /**
830             * Constructor.
831             *
832             * @param writer a Writer object providing the underlying stream
833             * @param delimiter the delimiter character for multi-valued properties
834             */
835            public PropertiesWriter(Writer writer, char delimiter)
836            {
837                super(writer);
838                this.delimiter = delimiter;
839            }
840    
841            /**
842             * Write a property.
843             *
844             * @param key the key of the property
845             * @param value the value of the property
846             *
847             * @throws IOException if an I/O error occurs
848             */
849            public void writeProperty(String key, Object value) throws IOException
850            {
851                writeProperty(key, value, false);
852            }
853    
854            /**
855             * Write a property.
856             *
857             * @param key The key of the property
858             * @param values The array of values of the property
859             *
860             * @throws IOException if an I/O error occurs
861             */
862            public void writeProperty(String key, List values) throws IOException
863            {
864                for (int i = 0; i < values.size(); i++)
865                {
866                    writeProperty(key, values.get(i));
867                }
868            }
869    
870            /**
871             * Writes the given property and its value. If the value happens to be a
872             * list, the <code>forceSingleLine</code> flag is evaluated. If it is
873             * set, all values are written on a single line using the list delimiter
874             * as separator.
875             *
876             * @param key the property key
877             * @param value the property value
878             * @param forceSingleLine the &quot;force single line&quot; flag
879             * @throws IOException if an error occurs
880             * @since 1.3
881             */
882            public void writeProperty(String key, Object value,
883                    boolean forceSingleLine) throws IOException
884            {
885                String v;
886    
887                if (value instanceof List)
888                {
889                    List values = (List) value;
890                    if (forceSingleLine)
891                    {
892                        v = makeSingleLineValue(values);
893                    }
894                    else
895                    {
896                        writeProperty(key, values);
897                        return;
898                    }
899                }
900                else
901                {
902                    v = escapeValue(value);
903                }
904    
905                write(escapeKey(key));
906                write(" = ");
907                write(v);
908    
909                writeln(null);
910            }
911    
912            /**
913             * Write a comment.
914             *
915             * @param comment the comment to write
916             * @throws IOException if an I/O error occurs
917             */
918            public void writeComment(String comment) throws IOException
919            {
920                writeln("# " + comment);
921            }
922    
923            /**
924             * Escape the separators in the key.
925             *
926             * @param key the key
927             * @return the escaped key
928             * @since 1.2
929             */
930            private String escapeKey(String key)
931            {
932                StringBuffer newkey = new StringBuffer();
933    
934                for (int i = 0; i < key.length(); i++)
935                {
936                    char c = key.charAt(i);
937    
938                    if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c))
939                    {
940                        // escape the separator
941                        newkey.append('\\');
942                        newkey.append(c);
943                    }
944                    else
945                    {
946                        newkey.append(c);
947                    }
948                }
949    
950                return newkey.toString();
951            }
952    
953            /**
954             * Escapes the given property value. Delimiter characters in the value
955             * will be escaped.
956             *
957             * @param value the property value
958             * @return the escaped property value
959             * @since 1.3
960             */
961            private String escapeValue(Object value)
962            {
963                String escapedValue = StringEscapeUtils.escapeJava(String.valueOf(value));
964                if (delimiter != 0)
965                {
966                    escapedValue = StringUtils.replace(escapedValue, String.valueOf(delimiter), ESCAPE + delimiter);
967                }
968                return escapedValue;
969            }
970    
971            /**
972             * Transforms a list of values into a single line value.
973             *
974             * @param values the list with the values
975             * @return a string with the single line value (can be <b>null</b>)
976             * @since 1.3
977             */
978            private String makeSingleLineValue(List values)
979            {
980                if (!values.isEmpty())
981                {
982                    Iterator it = values.iterator();
983                    String lastValue = escapeValue(it.next());
984                    StringBuffer buf = new StringBuffer(lastValue);
985                    while (it.hasNext())
986                    {
987                        // if the last value ended with an escape character, it has
988                        // to be escaped itself; otherwise the list delimiter will
989                        // be escaped
990                        if (lastValue.endsWith(ESCAPE))
991                        {
992                            buf.append(ESCAPE).append(ESCAPE);
993                        }
994                        buf.append(delimiter);
995                        lastValue = escapeValue(it.next());
996                        buf.append(lastValue);
997                    }
998                    return buf.toString();
999                }
1000                else
1001                {
1002                    return null;
1003                }
1004            }
1005    
1006            /**
1007             * Helper method for writing a line with the platform specific line
1008             * ending.
1009             *
1010             * @param s the content of the line (may be <b>null</b>)
1011             * @throws IOException if an error occurs
1012             * @since 1.3
1013             */
1014            public void writeln(String s) throws IOException
1015            {
1016                if (s != null)
1017                {
1018                    write(s);
1019                }
1020                write(LINE_SEPARATOR);
1021            }
1022    
1023        } // class PropertiesWriter
1024    
1025        /**
1026         * <p>Unescapes any Java literals found in the <code>String</code> to a
1027         * <code>Writer</code>.</p> This is a slightly modified version of the
1028         * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
1029         * drop escaped separators (i.e '\,').
1030         *
1031         * @param str  the <code>String</code> to unescape, may be null
1032         * @param delimiter the delimiter for multi-valued properties
1033         * @return the processed string
1034         * @throws IllegalArgumentException if the Writer is <code>null</code>
1035         */
1036        protected static String unescapeJava(String str, char delimiter)
1037        {
1038            if (str == null)
1039            {
1040                return null;
1041            }
1042            int sz = str.length();
1043            StringBuffer out = new StringBuffer(sz);
1044            StringBuffer unicode = new StringBuffer(UNICODE_LEN);
1045            boolean hadSlash = false;
1046            boolean inUnicode = false;
1047            for (int i = 0; i < sz; i++)
1048            {
1049                char ch = str.charAt(i);
1050                if (inUnicode)
1051                {
1052                    // if in unicode, then we're reading unicode
1053                    // values in somehow
1054                    unicode.append(ch);
1055                    if (unicode.length() == UNICODE_LEN)
1056                    {
1057                        // unicode now contains the four hex digits
1058                        // which represents our unicode character
1059                        try
1060                        {
1061                            int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
1062                            out.append((char) value);
1063                            unicode.setLength(0);
1064                            inUnicode = false;
1065                            hadSlash = false;
1066                        }
1067                        catch (NumberFormatException nfe)
1068                        {
1069                            throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
1070                        }
1071                    }
1072                    continue;
1073                }
1074    
1075                if (hadSlash)
1076                {
1077                    // handle an escaped value
1078                    hadSlash = false;
1079    
1080                    if (ch == '\\')
1081                    {
1082                        out.append('\\');
1083                    }
1084                    else if (ch == '\'')
1085                    {
1086                        out.append('\'');
1087                    }
1088                    else if (ch == '\"')
1089                    {
1090                        out.append('"');
1091                    }
1092                    else if (ch == 'r')
1093                    {
1094                        out.append('\r');
1095                    }
1096                    else if (ch == 'f')
1097                    {
1098                        out.append('\f');
1099                    }
1100                    else if (ch == 't')
1101                    {
1102                        out.append('\t');
1103                    }
1104                    else if (ch == 'n')
1105                    {
1106                        out.append('\n');
1107                    }
1108                    else if (ch == 'b')
1109                    {
1110                        out.append('\b');
1111                    }
1112                    else if (ch == delimiter)
1113                    {
1114                        out.append('\\');
1115                        out.append(delimiter);
1116                    }
1117                    else if (ch == 'u')
1118                    {
1119                        // uh-oh, we're in unicode country....
1120                        inUnicode = true;
1121                    }
1122                    else
1123                    {
1124                        out.append(ch);
1125                    }
1126    
1127                    continue;
1128                }
1129                else if (ch == '\\')
1130                {
1131                    hadSlash = true;
1132                    continue;
1133                }
1134                out.append(ch);
1135            }
1136    
1137            if (hadSlash)
1138            {
1139                // then we're in the weird case of a \ at the end of the
1140                // string, let's output it anyway.
1141                out.append('\\');
1142            }
1143    
1144            return out.toString();
1145        }
1146    
1147        /**
1148         * Helper method for loading an included properties file. This method is
1149         * called by <code>load()</code> when an <code>include</code> property
1150         * is encountered. It tries to resolve relative file names based on the
1151         * current base path. If this fails, a resolution based on the location of
1152         * this properties file is tried.
1153         *
1154         * @param fileName the name of the file to load
1155         * @throws ConfigurationException if loading fails
1156         */
1157        private void loadIncludeFile(String fileName) throws ConfigurationException
1158        {
1159            URL url = ConfigurationUtils.locate(getBasePath(), fileName);
1160            if (url == null)
1161            {
1162                URL baseURL = getURL();
1163                if (baseURL != null)
1164                {
1165                    url = ConfigurationUtils.locate(baseURL.toString(), fileName);
1166                }
1167            }
1168    
1169            if (url == null)
1170            {
1171                throw new ConfigurationException("Cannot resolve include file "
1172                        + fileName);
1173            }
1174            load(url);
1175        }
1176    }