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