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