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