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