You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2020/09/18 21:23:56 UTC

[commons-configuration] 05/06: Sort members.

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-configuration.git

commit bc15cb565eeaf2825b3ae9f7235a5680dc3f0f47
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Fri Sep 18 16:58:51 2020 -0400

    Sort members.
---
 .../configuration2/PropertiesConfiguration.java    | 2200 ++++++++++----------
 1 file changed, 1100 insertions(+), 1100 deletions(-)

diff --git a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
index e3d6967..c022ab8 100644
--- a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
+++ b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
@@ -206,518 +206,333 @@ public class PropertiesConfiguration extends BaseConfiguration
 {
 
     /**
-     * Defines default error handling for the special {@code "include"} key by throwing the given exception.
-     *
-     * @since 2.6
-     */
-    public static final ConfigurationConsumer<ConfigurationException> DEFAULT_INCLUDE_LISTENER = e -> { throw e; };
-
-    /**
-     * Defines error handling as a noop for the special {@code "include"} key.
-     *
-     * @since 2.6
-     */
-    public static final ConfigurationConsumer<ConfigurationException> NOOP_INCLUDE_LISTENER = e -> { /* noop */ };
-
-    /**
-     * The default encoding (ISO-8859-1 as specified by
-     * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
-     */
-    public static final String DEFAULT_ENCODING = "ISO-8859-1";
-
-    /** Constant for the supported comment characters.*/
-    static final String COMMENT_CHARS = "#!";
-
-    /** Constant for the default properties separator.*/
-    static final String DEFAULT_SEPARATOR = " = ";
-
-    /**
-     * A string with special characters that need to be unescaped when reading
-     * a properties file. {@code java.util.Properties} escapes these characters
-     * when writing out a properties file.
-     */
-    private static final String UNESCAPE_CHARACTERS = ":#=!\\\'\"";
-
-    /**
-     * This is the name of the property that can point to other
-     * properties file for including other properties files.
-     */
-    private static String include = "include";
-
-    /**
-     * This is the name of the property that can point to other
-     * properties file for including other properties files.
-     * <p>
-     * If the file is absent, processing continues normally.
-     * </p>
-     */
-    private static String includeOptional = "includeoptional";
-
-    /** The list of possible key/value separators */
-    private static final char[] SEPARATORS = new char[] {'=', ':'};
-
-    /** The white space characters used as key/value separators. */
-    private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'};
-
-    /** Constant for the platform specific line separator.*/
-    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
-
-    /** Constant for the radix of hex numbers.*/
-    private static final int HEX_RADIX = 16;
-
-    /** Constant for the length of a unicode literal.*/
-    private static final int UNICODE_LEN = 4;
-
-    /** Stores the layout object.*/
-    private PropertiesConfigurationLayout layout;
-
-    /** The include listener for the special {@code "include"} key. */
-    private ConfigurationConsumer<ConfigurationException> includeListener;
-
-    /** The IOFactory for creating readers and writers.*/
-    private IOFactory ioFactory;
-
-    /** The current {@code FileLocator}. */
-    private FileLocator locator;
-
-    /** Allow file inclusion or not */
-    private boolean includesAllowed = true;
-
-    /**
-     * Creates an empty PropertyConfiguration object which can be
-     * used to synthesize a new Properties file by adding values and
-     * then saving().
-     */
-    public PropertiesConfiguration()
-    {
-        installLayout(createLayout());
-    }
-
-    /**
-     * Gets the property value for including other properties files.
-     * By default it is "include".
-     *
-     * @return A String.
-     */
-    public static String getInclude()
-    {
-        return PropertiesConfiguration.include;
-    }
-
-    /**
-     * Gets the property value for including other properties files.
-     * By default it is "includeoptional".
      * <p>
-     * If the file is absent, processing continues normally.
+     * A default implementation of the {@code IOFactory} interface.
      * </p>
-     *
-     * @return A String.
-     * @since 2.5
-     */
-    public static String getIncludeOptional()
-    {
-        return PropertiesConfiguration.includeOptional;
-    }
-
-    /**
-     * Sets the property value for including other properties files.
-     * By default it is "include".
-     *
-     * @param inc A String.
-     */
-    public static void setInclude(final String inc)
-    {
-        PropertiesConfiguration.include = inc;
-    }
-
-    /**
-     * Sets the property value for including other properties files.
-     * By default it is "include".
      * <p>
-     * If the file is absent, processing continues normally.
+     * This class implements the {@code createXXXX()} methods defined by
+     * the {@code IOFactory} interface in a way that the default objects
+     * (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are
+     * returned. Customizing either the reader or the writer (or both) can be
+     * done by extending this class and overriding the corresponding
+     * {@code createXXXX()} method.
      * </p>
      *
-     * @param inc A String.
-     * @since 2.5
-     */
-    public static void setIncludeOptional(final String inc)
-    {
-        PropertiesConfiguration.includeOptional = inc;
-    }
-
-    /**
-     * Controls whether additional files can be loaded by the {@code include = <xxx>}
-     * statement or not. This is <b>true</b> per default.
-     *
-     * @param includesAllowed True if Includes are allowed.
-     */
-    public void setIncludesAllowed(final boolean includesAllowed)
-    {
-        this.includesAllowed = includesAllowed;
-    }
-
-    /**
-     * Reports the status of file inclusion.
-     *
-     * @return True if include files are loaded.
+     * @since 1.7
      */
-    public boolean isIncludesAllowed()
+    public static class DefaultIOFactory implements IOFactory
     {
-        return this.includesAllowed;
-    }
+        /**
+         * The singleton instance.
+         */
+        static final DefaultIOFactory INSTANCE = new DefaultIOFactory();
 
-    /**
-     * Return the comment header.
-     *
-     * @return the comment header
-     * @since 1.1
-     */
-    public String getHeader()
-    {
-        beginRead(false);
-        try
-        {
-            return getLayout().getHeaderComment();
-        }
-        finally
+        @Override
+        public PropertiesReader createPropertiesReader(final Reader in)
         {
-            endRead();
+            return new PropertiesReader(in);
         }
-    }
 
-    /**
-     * Set the comment header.
-     *
-     * @param header the header to use
-     * @since 1.1
-     */
-    public void setHeader(final String header)
-    {
-        beginWrite(false);
-        try
-        {
-            getLayout().setHeaderComment(header);
-        }
-        finally
+        @Override
+        public PropertiesWriter createPropertiesWriter(final Writer out,
+                final ListDelimiterHandler handler)
         {
-            endWrite();
+            return new PropertiesWriter(out, handler);
         }
     }
 
     /**
-     * Returns the footer comment. This is a comment at the very end of the
-     * file.
+     * <p>
+     * Definition of an interface that allows customization of read and write
+     * operations.
+     * </p>
+     * <p>
+     * For reading and writing properties files the inner classes
+     * {@code PropertiesReader} and {@code PropertiesWriter} are used.
+     * This interface defines factory methods for creating both a
+     * {@code PropertiesReader} and a {@code PropertiesWriter}. An
+     * object implementing this interface can be passed to the
+     * {@code setIOFactory()} method of
+     * {@code PropertiesConfiguration}. Every time the configuration is
+     * read or written the {@code IOFactory} is asked to create the
+     * appropriate reader or writer object. This provides an opportunity to
+     * inject custom reader or writer implementations.
+     * </p>
      *
-     * @return the footer comment
-     * @since 2.0
+     * @since 1.7
      */
-    public String getFooter()
+    public interface IOFactory
     {
-        beginRead(false);
-        try
-        {
-            return getLayout().getFooterComment();
-        }
-        finally
-        {
-            endRead();
-        }
-    }
+        /**
+         * Creates a {@code PropertiesReader} for reading a properties
+         * file. This method is called whenever the
+         * {@code PropertiesConfiguration} is loaded. The reader returned
+         * by this method is then used for parsing the properties file.
+         *
+         * @param in the underlying reader (of the properties file)
+         * @return the {@code PropertiesReader} for loading the
+         *         configuration
+         */
+        PropertiesReader createPropertiesReader(Reader in);
 
-    /**
-     * Sets the footer comment. If set, this comment is written after all
-     * properties at the end of the file.
-     *
-     * @param footer the footer comment
-     * @since 2.0
-     */
-    public void setFooter(final String footer)
-    {
-        beginWrite(false);
-        try
-        {
-            getLayout().setFooterComment(footer);
-        }
-        finally
-        {
-            endWrite();
-        }
+        /**
+         * Creates a {@code PropertiesWriter} for writing a properties
+         * file. This method is called before the
+         * {@code PropertiesConfiguration} is saved. The writer returned by
+         * this method is then used for writing the properties file.
+         *
+         * @param out the underlying writer (to the properties file)
+         * @param handler the list delimiter delimiter for list parsing
+         * @return the {@code PropertiesWriter} for saving the
+         *         configuration
+         */
+        PropertiesWriter createPropertiesWriter(Writer out,
+                ListDelimiterHandler handler);
     }
 
     /**
-     * Returns the associated layout object.
+     * An alternative {@link IOFactory} that tries to mimic the behavior of
+     * {@link java.util.Properties} (Jup) more closely. The goal is to allow both of
+     * them be used interchangeably when reading and writing properties files
+     * without losing or changing information.
+     * <p>
+     * It also has the option to <em>not</em> use Unicode escapes. When using UTF-8
+     * encoding (which is e.g. the new default for resource bundle properties files
+     * since Java 9), Unicode escapes are no longer required and avoiding them makes
+     * properties files more readable with regular text editors.
+     * <p>
+     * Some of the ways this implementation differs from {@link DefaultIOFactory}:
+     * <ul>
+     * <li>Trailing whitespace will not be trimmed from each line.</li>
+     * <li>Unknown escape sequences will have their backslash removed.</li>
+     * <li>{@code \b} is not a recognized escape sequence.</li>
+     * <li>Leading spaces in property values are preserved by escaping them.</li>
+     * <li>All natural lines (i.e. in the file) of a logical property line will have
+     * their leading whitespace trimmed.</li>
+     * <li>Natural lines that look like comment lines within a logical line are not
+     * treated as such; they're part of the property value.</li>
+     * </ul>
      *
-     * @return the associated layout object
-     * @since 1.3
+     * @since 2.4
      */
-    public PropertiesConfigurationLayout getLayout()
+    public static class JupIOFactory implements IOFactory
     {
-        return layout;
-    }
 
-    /**
-     * Sets the associated layout object.
-     *
-     * @param layout the new layout object; can be <b>null</b>, then a new
-     * layout object will be created
-     * @since 1.3
-     */
-    public void setLayout(final PropertiesConfigurationLayout layout)
-    {
-        installLayout(layout);
-    }
+        /**
+         * Whether characters less than {@code \u0020} and characters greater than
+         * {@code \u007E} in property keys or values should be escaped using
+         * Unicode escape sequences. Not necessary when e.g. writing as UTF-8.
+         */
+        private final boolean escapeUnicode;
 
-    /**
-     * Installs a layout object. It has to be ensured that the layout is
-     * registered as change listener at this configuration. If there is already
-     * a layout object installed, it has to be removed properly.
-     *
-     * @param layout the layout object to be installed
-     */
-    private void installLayout(final PropertiesConfigurationLayout layout)
-    {
-        // only one layout must exist
-        if (this.layout != null)
+        /**
+         * Constructs a new {@link JupIOFactory} with Unicode escaping.
+         */
+        public JupIOFactory()
         {
-            removeEventListener(ConfigurationEvent.ANY, this.layout);
+            this(true);
         }
 
-        if (layout == null)
-        {
-            this.layout = createLayout();
-        }
-        else
+        /**
+         * Constructs a new {@link JupIOFactory} with optional Unicode escaping. Whether
+         * Unicode escaping is required depends on the encoding used to save the
+         * properties file. E.g. for ISO-8859-1 this must be turned on, for UTF-8 it's
+         * not necessary. Unfortunately this factory can't determine the encoding on its
+         * own.
+         *
+         * @param escapeUnicode whether Unicode characters should be escaped
+         */
+        public JupIOFactory(final boolean escapeUnicode)
         {
-            this.layout = layout;
+            this.escapeUnicode = escapeUnicode;
         }
-        addEventListener(ConfigurationEvent.ANY, this.layout);
-    }
-
-    /**
-     * Creates a standard layout object. This configuration is initialized with
-     * such a standard layout.
-     *
-     * @return the newly created layout object
-     */
-    private PropertiesConfigurationLayout createLayout()
-    {
-        return new PropertiesConfigurationLayout();
-    }
 
-    /**
-     * Gets the current include listener, never null.
-     *
-     * @return the current include listener, never null.
-     * @since 2.6
-     */
-    public ConfigurationConsumer<ConfigurationException> getIncludeListener()
-    {
-        return includeListener != null ? includeListener : PropertiesConfiguration.DEFAULT_INCLUDE_LISTENER;
-    }
-
-    /**
-     * Returns the {@code IOFactory} to be used for creating readers and
-     * writers when loading or saving this configuration.
-     *
-     * @return the {@code IOFactory}
-     * @since 1.7
-     */
-    public IOFactory getIOFactory()
-    {
-        return ioFactory != null ? ioFactory : DefaultIOFactory.INSTANCE;
-    }
-
-    /**
-     * Sets the current include listener, may not be null.
-     *
-     * @param includeListener the current include listener, may not be null.
-     * @throws IllegalArgumentException if the {@code includeListener} is null.
-     * @since 2.6
-     */
-    public void setIncludeListener(final ConfigurationConsumer<ConfigurationException> includeListener)
-    {
-        if (includeListener == null)
+        @Override
+        public PropertiesReader createPropertiesReader(final Reader in)
         {
-            throw new IllegalArgumentException("includeListener must not be null.");
+            return new JupPropertiesReader(in);
         }
-        this.includeListener = includeListener;
-    }
 
-    /**
-     * Sets the {@code IOFactory} to be used for creating readers and
-     * writers when loading or saving this configuration. Using this method a
-     * client can customize the reader and writer classes used by the load and
-     * save operations. Note that this method must be called before invoking
-     * one of the {@code load()} and {@code save()} methods.
-     * Especially, if you want to use a custom {@code IOFactory} for
-     * changing the {@code PropertiesReader}, you cannot load the
-     * configuration data in the constructor.
-     *
-     * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>)
-     * @throws IllegalArgumentException if the {@code IOFactory} is
-     *         <b>null</b>
-     * @since 1.7
-     */
-    public void setIOFactory(final IOFactory ioFactory)
-    {
-        if (ioFactory == null)
+        @Override
+        public PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler)
         {
-            throw new IllegalArgumentException("IOFactory must not be null.");
+            return new JupPropertiesWriter(out, handler, escapeUnicode);
         }
 
-        this.ioFactory = ioFactory;
     }
 
     /**
-     * Stores the current {@code FileLocator} for a following IO operation. The
-     * {@code FileLocator} is needed to resolve include files with relative file
-     * names.
+     * A {@link PropertiesReader} that tries to mimic the behavior of
+     * {@link java.util.Properties}.
      *
-     * @param locator the current {@code FileLocator}
-     * @since 2.0
+     * @since 2.4
      */
-    @Override
-    public void initFileLocator(final FileLocator locator)
+    public static class JupPropertiesReader extends PropertiesReader
     {
-        this.locator = locator;
-    }
 
-    /**
-     * {@inheritDoc} This implementation delegates to the associated layout
-     * object which does the actual loading. Note that this method does not
-     * do any synchronization. This lies in the responsibility of the caller.
-     * (Typically, the caller is a {@code FileHandler} object which takes
-     * care for proper synchronization.)
-     *
-     * @since 2.0
-     */
-    @Override
-    public void read(final Reader in) throws ConfigurationException, IOException
-    {
-        getLayout().load(this, in);
-    }
+        /**
+         * Constructor.
+         *
+         * @param reader A Reader.
+         */
+        public JupPropertiesReader(final Reader reader)
+        {
+            super(reader);
+        }
 
-    /**
-     * {@inheritDoc} This implementation delegates to the associated layout
-     * object which does the actual saving. Note that, analogous to
-     * {@link #read(Reader)}, this method does not do any synchronization.
-     *
-     * @since 2.0
-     */
-    @Override
-    public void write(final Writer out) throws ConfigurationException, IOException
-    {
-        getLayout().save(this, out);
-    }
 
-    /**
-     * Creates a copy of this object.
-     *
-     * @return the copy
-     */
-    @Override
-    public Object clone()
-    {
-        final PropertiesConfiguration copy = (PropertiesConfiguration) super.clone();
-        if (layout != null)
+        @Override
+        protected void parseProperty(final String line)
         {
-            copy.setLayout(new PropertiesConfigurationLayout(layout));
+            final String[] property = doParseProperty(line, false);
+            initPropertyName(property[0]);
+            initPropertyValue(property[1]);
+            initPropertySeparator(property[2]);
         }
-        return copy;
-    }
-
-    /**
-     * This method is invoked by the associated
-     * {@link PropertiesConfigurationLayout} object for each
-     * property definition detected in the parsed properties file. Its task is
-     * to check whether this is a special property definition (e.g. the
-     * {@code include} property). If not, the property must be added to
-     * this configuration. The return value indicates whether the property
-     * should be treated as a normal property. If it is <b>false</b>, the
-     * layout object will ignore this property.
-     *
-     * @param key the property key
-     * @param value the property value
-     * @param seenStack the stack of seen include URLs
-     * @return a flag whether this is a normal property
-     * @throws ConfigurationException if an error occurs
-     * @since 1.3
-     */
-    boolean propertyLoaded(final String key, final String value, final Deque<URL> seenStack)
-            throws ConfigurationException
-    {
-        boolean result;
 
-        if (StringUtils.isNotEmpty(getInclude())
-                && key.equalsIgnoreCase(getInclude()))
+        @Override
+        public String readProperty() throws IOException
         {
-            if (isIncludesAllowed())
+            getCommentLines().clear();
+            final StringBuilder buffer = new StringBuilder();
+
+            while (true)
             {
-                final Collection<String> files =
-                        getListDelimiterHandler().split(value, true);
-                for (final String f : files)
+                String line = readLine();
+                if (line == null)
                 {
-                    loadIncludeFile(interpolate(f), false, seenStack);
+                    // EOF
+                    if (buffer.length() > 0)
+                    {
+                        break;
+                    }
+                    return null;
                 }
-            }
-            result = false;
-        }
 
-        else if (StringUtils.isNotEmpty(getIncludeOptional())
-            && key.equalsIgnoreCase(getIncludeOptional()))
-        {
-            if (isIncludesAllowed())
-            {
-                final Collection<String> files =
-                        getListDelimiterHandler().split(value, true);
-                for (final String f : files)
+                // while a property line continues there are no comments (even if the line from
+                // the file looks like one)
+                if (isCommentLine(line) && (buffer.length() == 0))
                 {
-                    loadIncludeFile(interpolate(f), true, seenStack);
+                    getCommentLines().add(line);
+                    continue;
+                }
+
+                // while property line continues left trim all following lines read from the
+                // file
+                if (buffer.length() > 0)
+                {
+                    // index of the first non-whitespace character
+                    int i;
+                    for (i = 0; i < line.length(); i++)
+                    {
+                        if (!Character.isWhitespace(line.charAt(i)))
+                        {
+                            break;
+                        }
+                    }
+
+                    line = line.substring(i);
+                }
+
+                if (checkCombineLines(line))
+                {
+                    line = line.substring(0, line.length() - 1);
+                    buffer.append(line);
+                }
+                else
+                {
+                    buffer.append(line);
+                    break;
                 }
             }
-            result = false;
+            return buffer.toString();
         }
 
-        else
+        @Override
+        protected String unescapePropertyValue(final String value)
         {
-            addPropertyInternal(key, value);
-            result = true;
+            return unescapeJava(value, true);
         }
 
-        return result;
     }
 
-    /**
-     * Tests whether a line is a comment, i.e. whether it starts with a comment
-     * character.
-     *
-     * @param line the line
-     * @return a flag if this is a comment line
-     * @since 1.3
-     */
-    static boolean isCommentLine(final String line)
-    {
-        final String s = line.trim();
-        // blanc lines are also treated as comment lines
-        return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
-    }
+    /**
+     * A {@link PropertiesWriter} that tries to mimic the behavior of
+     * {@link java.util.Properties}.
+     *
+     * @since 2.4
+     */
+    public static class JupPropertiesWriter extends PropertiesWriter
+    {
+
+        /**
+         * The starting ASCII printable character.
+         */
+        private static final int PRINTABLE_INDEX_END = 0x7e;
+
+        /**
+         * The ending ASCII printable character.
+         */
+        private static final int PRINTABLE_INDEX_START = 0x20;
+
+        /**
+         * A UnicodeEscaper for characters outside the ASCII printable range.
+         */
+        private static final UnicodeEscaper ESCAPER = UnicodeEscaper.outsideOf(PRINTABLE_INDEX_START,
+            PRINTABLE_INDEX_END);
+
+        /**
+         * Characters that need to be escaped when wring a properties file.
+         */
+        private static final Map<CharSequence, CharSequence> JUP_CHARS_ESCAPE;
+        static
+        {
+            final Map<CharSequence, CharSequence> initialMap = new HashMap<>();
+            initialMap.put("\\", "\\\\");
+            initialMap.put("\n", "\\n");
+            initialMap.put("\t", "\\t");
+            initialMap.put("\f", "\\f");
+            initialMap.put("\r", "\\r");
+            JUP_CHARS_ESCAPE = Collections.unmodifiableMap(initialMap);
+        }
+
+        /**
+         * Creates a new instance of {@code JupPropertiesWriter}.
+         *
+         * @param writer a Writer object providing the underlying stream
+         * @param delHandler the delimiter handler for dealing with properties with
+         *        multiple values
+         * @param escapeUnicode whether Unicode characters should be escaped using
+         *        Unicode escapes
+         */
+        public JupPropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler,
+            final boolean escapeUnicode)
+        {
+            super(writer, delHandler, value -> {
+                String valueString = String.valueOf(value);
+
+                CharSequenceTranslator translator;
+                if (escapeUnicode)
+                {
+                    translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE), ESCAPER);
+                }
+                else
+                {
+                    translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE));
+                }
+
+                valueString = translator.translate(valueString);
+
+                // escape the first leading space to preserve it (and all after it)
+                if (valueString.startsWith(" "))
+                {
+                    valueString = "\\" + valueString;
+                }
 
-    /**
-     * Returns the number of trailing backslashes. This is sometimes needed for
-     * the correct handling of escape characters.
-     *
-     * @param line the string to investigate
-     * @return the number of trailing backslashes
-     */
-    private static int countTrailingBS(final String line)
-    {
-        int bsCount = 0;
-        for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
-        {
-            bsCount++;
+                return valueString;
+            });
         }
 
-        return bsCount;
     }
 
     /**
@@ -743,99 +558,70 @@ public class PropertiesConfiguration extends BaseConfiguration
         /** Constant for the index of the group for the separator. */
         private static final int IDX_SEPARATOR = 3;
 
-        /** Stores the comment lines for the currently processed property.*/
-        private final List<String> commentLines;
-
-        /** Stores the name of the last read property.*/
-        private String propertyName;
-
-        /** Stores the value of the last read property.*/
-        private String propertyValue;
-
-        /** Stores the property separator of the last read property.*/
-        private String propertySeparator = DEFAULT_SEPARATOR;
-
         /**
-         * Constructor.
+         * Checks if the passed in line should be combined with the following.
+         * This is true, if the line ends with an odd number of backslashes.
          *
-         * @param reader A Reader.
+         * @param line the line
+         * @return a flag if the lines should be combined
          */
-        public PropertiesReader(final Reader reader)
+        static boolean checkCombineLines(final String line)
         {
-            super(reader);
-            commentLines = new ArrayList<>();
+            return countTrailingBS(line) % 2 != 0;
         }
 
         /**
-         * Reads a property line. Returns null if Stream is
-         * at EOF. Concatenates lines ending with "\".
-         * Skips lines beginning with "#" or "!" and empty lines.
-         * The return value is a property definition ({@code &lt;name&gt;}
-         * = {@code &lt;value&gt;})
-         *
-         * @return A string containing a property value or null
+         * Parse a property line and return the key, the value, and the separator in an
+         * array.
          *
-         * @throws IOException in case of an I/O error
+         * @param line the line to parse
+         * @param trimValue flag whether the value is to be trimmed
+         * @return an array with the property's key, value, and separator
          */
-        public String readProperty() throws IOException
+        static String[] doParseProperty(final String line, final boolean trimValue)
         {
-            commentLines.clear();
-            final StringBuilder buffer = new StringBuilder();
+            final Matcher matcher = PROPERTY_PATTERN.matcher(line);
 
-            while (true)
+            final String[] result = {"", "", ""};
+
+            if (matcher.matches())
             {
-                String line = readLine();
-                if (line == null)
-                {
-                    // EOF
-                    return null;
-                }
+                result[0] = matcher.group(IDX_KEY).trim();
 
-                if (isCommentLine(line))
+                String value = matcher.group(IDX_VALUE);
+                if (trimValue)
                 {
-                    commentLines.add(line);
-                    continue;
+                    value = value.trim();
                 }
+                result[1] = value;
 
-                line = line.trim();
-
-                if (checkCombineLines(line))
-                {
-                    line = line.substring(0, line.length() - 1);
-                    buffer.append(line);
-                }
-                else
-                {
-                    buffer.append(line);
-                    break;
-                }
+                result[2] = matcher.group(IDX_SEPARATOR);
             }
-            return buffer.toString();
+
+            return result;
         }
 
+        /** Stores the comment lines for the currently processed property.*/
+        private final List<String> commentLines;
+
+        /** Stores the name of the last read property.*/
+        private String propertyName;
+
+        /** Stores the value of the last read property.*/
+        private String propertyValue;
+
+        /** Stores the property separator of the last read property.*/
+        private String propertySeparator = DEFAULT_SEPARATOR;
+
         /**
-         * Parses the next property from the input stream and stores the found
-         * name and value in internal fields. These fields can be obtained using
-         * the provided getter methods. The return value indicates whether EOF
-         * was reached (<b>false</b>) or whether further properties are
-         * available (<b>true</b>).
+         * Constructor.
          *
-         * @return a flag if further properties are available
-         * @throws IOException if an error occurs
-         * @since 1.3
+         * @param reader A Reader.
          */
-        public boolean nextProperty() throws IOException
+        public PropertiesReader(final Reader reader)
         {
-            final String line = readProperty();
-
-            if (line == null)
-            {
-                return false; // EOF
-            }
-
-            // parse the line
-            parseProperty(line);
-            return true;
+            super(reader);
+            commentLines = new ArrayList<>();
         }
 
         /**
@@ -864,6 +650,19 @@ public class PropertiesConfiguration extends BaseConfiguration
         }
 
         /**
+         * Returns the separator that was used for the last read property. The
+         * separator can be stored so that it can later be restored when saving
+         * the configuration.
+         *
+         * @return the separator for the last read property
+         * @since 1.7
+         */
+        public String getPropertySeparator()
+        {
+            return propertySeparator;
+        }
+
+        /**
          * Returns the value of the last read property. This method can be
          * called after {@link #nextProperty()} was invoked and
          * its return value was <b>true</b>.
@@ -877,16 +676,70 @@ public class PropertiesConfiguration extends BaseConfiguration
         }
 
         /**
-         * Returns the separator that was used for the last read property. The
-         * separator can be stored so that it can later be restored when saving
-         * the configuration.
+         * Sets the name of the current property. This method can be called by
+         * {@code parseProperty()} for storing the results of the parse
+         * operation. It also ensures that the property key is correctly
+         * escaped.
          *
-         * @return the separator for the last read property
+         * @param name the name of the current property
          * @since 1.7
          */
-        public String getPropertySeparator()
+        protected void initPropertyName(final String name)
         {
-            return propertySeparator;
+            propertyName = unescapePropertyName(name);
+        }
+
+        /**
+         * Sets the separator of the current property. This method can be called
+         * by {@code parseProperty()}. It allows the associated layout
+         * object to keep track of the property separators. When saving the
+         * configuration the separators can be restored.
+         *
+         * @param value the separator used for the current property
+         * @since 1.7
+         */
+        protected void initPropertySeparator(final String value)
+        {
+            propertySeparator = value;
+        }
+
+        /**
+         * Sets the value of the current property. This method can be called by
+         * {@code parseProperty()} for storing the results of the parse
+         * operation. It also ensures that the property value is correctly
+         * escaped.
+         *
+         * @param value the value of the current property
+         * @since 1.7
+         */
+        protected void initPropertyValue(final String value)
+        {
+            propertyValue = unescapePropertyValue(value);
+        }
+
+        /**
+         * Parses the next property from the input stream and stores the found
+         * name and value in internal fields. These fields can be obtained using
+         * the provided getter methods. The return value indicates whether EOF
+         * was reached (<b>false</b>) or whether further properties are
+         * available (<b>true</b>).
+         *
+         * @return a flag if further properties are available
+         * @throws IOException if an error occurs
+         * @since 1.3
+         */
+        public boolean nextProperty() throws IOException
+        {
+            final String line = readProperty();
+
+            if (line == null)
+            {
+                return false; // EOF
+            }
+
+            // parse the line
+            parseProperty(line);
+            return true;
         }
 
         /**
@@ -908,17 +761,50 @@ public class PropertiesConfiguration extends BaseConfiguration
         }
 
         /**
-         * Sets the name of the current property. This method can be called by
-         * {@code parseProperty()} for storing the results of the parse
-         * operation. It also ensures that the property key is correctly
-         * escaped.
+         * Reads a property line. Returns null if Stream is
+         * at EOF. Concatenates lines ending with "\".
+         * Skips lines beginning with "#" or "!" and empty lines.
+         * The return value is a property definition ({@code &lt;name&gt;}
+         * = {@code &lt;value&gt;})
          *
-         * @param name the name of the current property
-         * @since 1.7
+         * @return A string containing a property value or null
+         *
+         * @throws IOException in case of an I/O error
          */
-        protected void initPropertyName(final String name)
+        public String readProperty() throws IOException
         {
-            propertyName = unescapePropertyName(name);
+            commentLines.clear();
+            final StringBuilder buffer = new StringBuilder();
+
+            while (true)
+            {
+                String line = readLine();
+                if (line == null)
+                {
+                    // EOF
+                    return null;
+                }
+
+                if (isCommentLine(line))
+                {
+                    commentLines.add(line);
+                    continue;
+                }
+
+                line = line.trim();
+
+                if (checkCombineLines(line))
+                {
+                    line = line.substring(0, line.length() - 1);
+                    buffer.append(line);
+                }
+                else
+                {
+                    buffer.append(line);
+                    break;
+                }
+            }
+            return buffer.toString();
         }
 
         /**
@@ -934,20 +820,6 @@ public class PropertiesConfiguration extends BaseConfiguration
         }
 
         /**
-         * Sets the value of the current property. This method can be called by
-         * {@code parseProperty()} for storing the results of the parse
-         * operation. It also ensures that the property value is correctly
-         * escaped.
-         *
-         * @param value the value of the current property
-         * @since 1.7
-         */
-        protected void initPropertyValue(final String value)
-        {
-            propertyValue = unescapePropertyValue(value);
-        }
-
-        /**
          * Performs unescaping on the given property value.
          *
          * @param value the property value
@@ -958,63 +830,6 @@ public class PropertiesConfiguration extends BaseConfiguration
         {
             return unescapeJava(value);
         }
-
-        /**
-         * Sets the separator of the current property. This method can be called
-         * by {@code parseProperty()}. It allows the associated layout
-         * object to keep track of the property separators. When saving the
-         * configuration the separators can be restored.
-         *
-         * @param value the separator used for the current property
-         * @since 1.7
-         */
-        protected void initPropertySeparator(final String value)
-        {
-            propertySeparator = value;
-        }
-
-        /**
-         * Checks if the passed in line should be combined with the following.
-         * This is true, if the line ends with an odd number of backslashes.
-         *
-         * @param line the line
-         * @return a flag if the lines should be combined
-         */
-        static boolean checkCombineLines(final String line)
-        {
-            return countTrailingBS(line) % 2 != 0;
-        }
-
-        /**
-         * Parse a property line and return the key, the value, and the separator in an
-         * array.
-         *
-         * @param line the line to parse
-         * @param trimValue flag whether the value is to be trimmed
-         * @return an array with the property's key, value, and separator
-         */
-        static String[] doParseProperty(final String line, final boolean trimValue)
-        {
-            final Matcher matcher = PROPERTY_PATTERN.matcher(line);
-
-            final String[] result = {"", "", ""};
-
-            if (matcher.matches())
-            {
-                result[0] = matcher.group(IDX_KEY).trim();
-
-                String value = matcher.group(IDX_VALUE);
-                if (trimValue)
-                {
-                    value = value.trim();
-                }
-                result[1] = value;
-
-                result[2] = matcher.group(IDX_SEPARATOR);
-            }
-
-            return result;
-        }
     } // class PropertiesReader
 
     /**
@@ -1065,218 +880,40 @@ public class PropertiesConfiguration extends BaseConfiguration
         private final ListDelimiterHandler delimiterHandler;
 
         /** The separator to be used for the current property. */
-        private String currentSeparator;
-
-        /** The global separator. If set, it overrides the current separator.*/
-        private String globalSeparator;
-
-        /** The line separator.*/
-        private String lineSeparator;
-
-        /**
-         * Creates a new instance of {@code PropertiesWriter}.
-         *
-         * @param writer a Writer object providing the underlying stream
-         * @param delHandler the delimiter handler for dealing with properties
-         *        with multiple values
-         */
-        public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler)
-        {
-            this(writer, delHandler, DEFAULT_TRANSFORMER);
-        }
-
-        /**
-         * Creates a new instance of {@code PropertiesWriter}.
-         *
-         * @param writer a Writer object providing the underlying stream
-         * @param delHandler the delimiter handler for dealing with properties
-         *        with multiple values
-         * @param valueTransformer the value transformer used to escape property values
-         */
-        public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler,
-            final ValueTransformer valueTransformer)
-        {
-            super(writer);
-            delimiterHandler = delHandler;
-            this.valueTransformer = valueTransformer;
-        }
-
-        /**
-         * Returns the delimiter handler for properties with multiple values.
-         * This object is used to escape property values so that they can be
-         * read in correctly the next time they are loaded.
-         *
-         * @return the delimiter handler for properties with multiple values
-         * @since 2.0
-         */
-        public ListDelimiterHandler getDelimiterHandler()
-        {
-            return delimiterHandler;
-        }
-
-        /**
-         * Returns the current property separator.
-         *
-         * @return the current property separator
-         * @since 1.7
-         */
-        public String getCurrentSeparator()
-        {
-            return currentSeparator;
-        }
-
-        /**
-         * Sets the current property separator. This separator is used when
-         * writing the next property.
-         *
-         * @param currentSeparator the current property separator
-         * @since 1.7
-         */
-        public void setCurrentSeparator(final String currentSeparator)
-        {
-            this.currentSeparator = currentSeparator;
-        }
-
-        /**
-         * Returns the global property separator.
-         *
-         * @return the global property separator
-         * @since 1.7
-         */
-        public String getGlobalSeparator()
-        {
-            return globalSeparator;
-        }
-
-        /**
-         * Sets the global property separator. This separator corresponds to the
-         * {@code globalSeparator} property of
-         * {@link PropertiesConfigurationLayout}. It defines the separator to be
-         * used for all properties. If it is undefined, the current separator is
-         * used.
-         *
-         * @param globalSeparator the global property separator
-         * @since 1.7
-         */
-        public void setGlobalSeparator(final String globalSeparator)
-        {
-            this.globalSeparator = globalSeparator;
-        }
-
-        /**
-         * Returns the line separator.
-         *
-         * @return the line separator
-         * @since 1.7
-         */
-        public String getLineSeparator()
-        {
-            return lineSeparator != null ? lineSeparator : LINE_SEPARATOR;
-        }
-
-        /**
-         * Sets the line separator. Each line written by this writer is
-         * terminated with this separator. If not set, the platform-specific
-         * line separator is used.
-         *
-         * @param lineSeparator the line separator to be used
-         * @since 1.7
-         */
-        public void setLineSeparator(final String lineSeparator)
-        {
-            this.lineSeparator = lineSeparator;
-        }
-
-        /**
-         * Write a property.
-         *
-         * @param key the key of the property
-         * @param value the value of the property
-         *
-         * @throws IOException if an I/O error occurs
-         */
-        public void writeProperty(final String key, final Object value) throws IOException
-        {
-            writeProperty(key, value, false);
-        }
-
-        /**
-         * Write a property.
-         *
-         * @param key The key of the property
-         * @param values The array of values of the property
-         *
-         * @throws IOException if an I/O error occurs
-         */
-        public void writeProperty(final String key, final List<?> values) throws IOException
-        {
-            for (int i = 0; i < values.size(); i++)
-            {
-                writeProperty(key, values.get(i));
-            }
-        }
-
-        /**
-         * Writes the given property and its value. If the value happens to be a
-         * list, the {@code forceSingleLine} flag is evaluated. If it is
-         * set, all values are written on a single line using the list delimiter
-         * as separator.
-         *
-         * @param key the property key
-         * @param value the property value
-         * @param forceSingleLine the &quot;force single line&quot; flag
-         * @throws IOException if an error occurs
-         * @since 1.3
-         */
-        public void writeProperty(final String key, final Object value,
-                final boolean forceSingleLine) throws IOException
-        {
-            String v;
+        private String currentSeparator;
 
-            if (value instanceof List)
-            {
-                v = null;
-                final List<?> values = (List<?>) value;
-                if (forceSingleLine)
-                {
-                    try
-                    {
-                        v = String.valueOf(getDelimiterHandler()
-                                        .escapeList(values, valueTransformer));
-                    }
-                    catch (final UnsupportedOperationException uoex)
-                    {
-                        // the handler may not support escaping lists,
-                        // then the list is written in multiple lines
-                    }
-                }
-                if (v == null)
-                {
-                    writeProperty(key, values);
-                    return;
-                }
-            }
-            else
-            {
-                v = String.valueOf(getDelimiterHandler().escape(value, valueTransformer));
-            }
+        /** The global separator. If set, it overrides the current separator.*/
+        private String globalSeparator;
 
-            write(escapeKey(key));
-            write(fetchSeparator(key, value));
-            write(v);
+        /** The line separator.*/
+        private String lineSeparator;
 
-            writeln(null);
+        /**
+         * Creates a new instance of {@code PropertiesWriter}.
+         *
+         * @param writer a Writer object providing the underlying stream
+         * @param delHandler the delimiter handler for dealing with properties
+         *        with multiple values
+         */
+        public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler)
+        {
+            this(writer, delHandler, DEFAULT_TRANSFORMER);
         }
 
         /**
-         * Write a comment.
+         * Creates a new instance of {@code PropertiesWriter}.
          *
-         * @param comment the comment to write
-         * @throws IOException if an I/O error occurs
+         * @param writer a Writer object providing the underlying stream
+         * @param delHandler the delimiter handler for dealing with properties
+         *        with multiple values
+         * @param valueTransformer the value transformer used to escape property values
          */
-        public void writeComment(final String comment) throws IOException
+        public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler,
+            final ValueTransformer valueTransformer)
         {
-            writeln("# " + comment);
+            super(writer);
+            delimiterHandler = delHandler;
+            this.valueTransformer = valueTransformer;
         }
 
         /**
@@ -1313,23 +950,6 @@ public class PropertiesConfiguration extends BaseConfiguration
         }
 
         /**
-         * Helper method for writing a line with the platform specific line
-         * ending.
-         *
-         * @param s the content of the line (may be <b>null</b>)
-         * @throws IOException if an error occurs
-         * @since 1.3
-         */
-        public void writeln(final String s) throws IOException
-        {
-            if (s != null)
-            {
-                write(s);
-            }
-            write(getLineSeparator());
-        }
-
-        /**
          * Returns the separator to be used for the given property. This method
          * is called by {@code writeProperty()}. The string returned here
          * is used as separator between the property key and its value. Per
@@ -1349,336 +969,364 @@ public class PropertiesConfiguration extends BaseConfiguration
             return getGlobalSeparator() != null ? getGlobalSeparator()
                     : StringUtils.defaultString(getCurrentSeparator());
         }
-    } // class PropertiesWriter
 
-    /**
-     * <p>
-     * Definition of an interface that allows customization of read and write
-     * operations.
-     * </p>
-     * <p>
-     * For reading and writing properties files the inner classes
-     * {@code PropertiesReader} and {@code PropertiesWriter} are used.
-     * This interface defines factory methods for creating both a
-     * {@code PropertiesReader} and a {@code PropertiesWriter}. An
-     * object implementing this interface can be passed to the
-     * {@code setIOFactory()} method of
-     * {@code PropertiesConfiguration}. Every time the configuration is
-     * read or written the {@code IOFactory} is asked to create the
-     * appropriate reader or writer object. This provides an opportunity to
-     * inject custom reader or writer implementations.
-     * </p>
-     *
-     * @since 1.7
-     */
-    public interface IOFactory
-    {
         /**
-         * Creates a {@code PropertiesReader} for reading a properties
-         * file. This method is called whenever the
-         * {@code PropertiesConfiguration} is loaded. The reader returned
-         * by this method is then used for parsing the properties file.
+         * Returns the current property separator.
          *
-         * @param in the underlying reader (of the properties file)
-         * @return the {@code PropertiesReader} for loading the
-         *         configuration
+         * @return the current property separator
+         * @since 1.7
          */
-        PropertiesReader createPropertiesReader(Reader in);
+        public String getCurrentSeparator()
+        {
+            return currentSeparator;
+        }
 
         /**
-         * Creates a {@code PropertiesWriter} for writing a properties
-         * file. This method is called before the
-         * {@code PropertiesConfiguration} is saved. The writer returned by
-         * this method is then used for writing the properties file.
+         * Returns the delimiter handler for properties with multiple values.
+         * This object is used to escape property values so that they can be
+         * read in correctly the next time they are loaded.
          *
-         * @param out the underlying writer (to the properties file)
-         * @param handler the list delimiter delimiter for list parsing
-         * @return the {@code PropertiesWriter} for saving the
-         *         configuration
+         * @return the delimiter handler for properties with multiple values
+         * @since 2.0
          */
-        PropertiesWriter createPropertiesWriter(Writer out,
-                ListDelimiterHandler handler);
-    }
+        public ListDelimiterHandler getDelimiterHandler()
+        {
+            return delimiterHandler;
+        }
 
-    /**
-     * <p>
-     * A default implementation of the {@code IOFactory} interface.
-     * </p>
-     * <p>
-     * This class implements the {@code createXXXX()} methods defined by
-     * the {@code IOFactory} interface in a way that the default objects
-     * (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are
-     * returned. Customizing either the reader or the writer (or both) can be
-     * done by extending this class and overriding the corresponding
-     * {@code createXXXX()} method.
-     * </p>
-     *
-     * @since 1.7
-     */
-    public static class DefaultIOFactory implements IOFactory
-    {
         /**
-         * The singleton instance.
+         * Returns the global property separator.
+         *
+         * @return the global property separator
+         * @since 1.7
          */
-        static final DefaultIOFactory INSTANCE = new DefaultIOFactory();
-
-        @Override
-        public PropertiesReader createPropertiesReader(final Reader in)
+        public String getGlobalSeparator()
         {
-            return new PropertiesReader(in);
+            return globalSeparator;
         }
 
-        @Override
-        public PropertiesWriter createPropertiesWriter(final Writer out,
-                final ListDelimiterHandler handler)
+        /**
+         * Returns the line separator.
+         *
+         * @return the line separator
+         * @since 1.7
+         */
+        public String getLineSeparator()
         {
-            return new PropertiesWriter(out, handler);
+            return lineSeparator != null ? lineSeparator : LINE_SEPARATOR;
         }
-    }
 
-    /**
-     * An alternative {@link IOFactory} that tries to mimic the behavior of
-     * {@link java.util.Properties} (Jup) more closely. The goal is to allow both of
-     * them be used interchangeably when reading and writing properties files
-     * without losing or changing information.
-     * <p>
-     * It also has the option to <em>not</em> use Unicode escapes. When using UTF-8
-     * encoding (which is e.g. the new default for resource bundle properties files
-     * since Java 9), Unicode escapes are no longer required and avoiding them makes
-     * properties files more readable with regular text editors.
-     * <p>
-     * Some of the ways this implementation differs from {@link DefaultIOFactory}:
-     * <ul>
-     * <li>Trailing whitespace will not be trimmed from each line.</li>
-     * <li>Unknown escape sequences will have their backslash removed.</li>
-     * <li>{@code \b} is not a recognized escape sequence.</li>
-     * <li>Leading spaces in property values are preserved by escaping them.</li>
-     * <li>All natural lines (i.e. in the file) of a logical property line will have
-     * their leading whitespace trimmed.</li>
-     * <li>Natural lines that look like comment lines within a logical line are not
-     * treated as such; they're part of the property value.</li>
-     * </ul>
-     *
-     * @since 2.4
-     */
-    public static class JupIOFactory implements IOFactory
-    {
+        /**
+         * Sets the current property separator. This separator is used when
+         * writing the next property.
+         *
+         * @param currentSeparator the current property separator
+         * @since 1.7
+         */
+        public void setCurrentSeparator(final String currentSeparator)
+        {
+            this.currentSeparator = currentSeparator;
+        }
 
         /**
-         * Whether characters less than {@code \u0020} and characters greater than
-         * {@code \u007E} in property keys or values should be escaped using
-         * Unicode escape sequences. Not necessary when e.g. writing as UTF-8.
+         * Sets the global property separator. This separator corresponds to the
+         * {@code globalSeparator} property of
+         * {@link PropertiesConfigurationLayout}. It defines the separator to be
+         * used for all properties. If it is undefined, the current separator is
+         * used.
+         *
+         * @param globalSeparator the global property separator
+         * @since 1.7
          */
-        private final boolean escapeUnicode;
+        public void setGlobalSeparator(final String globalSeparator)
+        {
+            this.globalSeparator = globalSeparator;
+        }
 
         /**
-         * Constructs a new {@link JupIOFactory} with Unicode escaping.
+         * Sets the line separator. Each line written by this writer is
+         * terminated with this separator. If not set, the platform-specific
+         * line separator is used.
+         *
+         * @param lineSeparator the line separator to be used
+         * @since 1.7
          */
-        public JupIOFactory()
+        public void setLineSeparator(final String lineSeparator)
         {
-            this(true);
+            this.lineSeparator = lineSeparator;
         }
 
         /**
-         * Constructs a new {@link JupIOFactory} with optional Unicode escaping. Whether
-         * Unicode escaping is required depends on the encoding used to save the
-         * properties file. E.g. for ISO-8859-1 this must be turned on, for UTF-8 it's
-         * not necessary. Unfortunately this factory can't determine the encoding on its
-         * own.
+         * Write a comment.
          *
-         * @param escapeUnicode whether Unicode characters should be escaped
+         * @param comment the comment to write
+         * @throws IOException if an I/O error occurs
          */
-        public JupIOFactory(final boolean escapeUnicode)
+        public void writeComment(final String comment) throws IOException
         {
-            this.escapeUnicode = escapeUnicode;
+            writeln("# " + comment);
         }
 
-        @Override
-        public PropertiesReader createPropertiesReader(final Reader in)
+        /**
+         * Helper method for writing a line with the platform specific line
+         * ending.
+         *
+         * @param s the content of the line (may be <b>null</b>)
+         * @throws IOException if an error occurs
+         * @since 1.3
+         */
+        public void writeln(final String s) throws IOException
         {
-            return new JupPropertiesReader(in);
+            if (s != null)
+            {
+                write(s);
+            }
+            write(getLineSeparator());
         }
 
-        @Override
-        public PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler)
+        /**
+         * Write a property.
+         *
+         * @param key The key of the property
+         * @param values The array of values of the property
+         *
+         * @throws IOException if an I/O error occurs
+         */
+        public void writeProperty(final String key, final List<?> values) throws IOException
         {
-            return new JupPropertiesWriter(out, handler, escapeUnicode);
+            for (int i = 0; i < values.size(); i++)
+            {
+                writeProperty(key, values.get(i));
+            }
         }
 
-    }
-
-    /**
-     * A {@link PropertiesReader} that tries to mimic the behavior of
-     * {@link java.util.Properties}.
-     *
-     * @since 2.4
-     */
-    public static class JupPropertiesReader extends PropertiesReader
-    {
-
         /**
-         * Constructor.
+         * Write a property.
          *
-         * @param reader A Reader.
+         * @param key the key of the property
+         * @param value the value of the property
+         *
+         * @throws IOException if an I/O error occurs
          */
-        public JupPropertiesReader(final Reader reader)
+        public void writeProperty(final String key, final Object value) throws IOException
         {
-            super(reader);
+            writeProperty(key, value, false);
         }
 
-
-        @Override
-        public String readProperty() throws IOException
+        /**
+         * Writes the given property and its value. If the value happens to be a
+         * list, the {@code forceSingleLine} flag is evaluated. If it is
+         * set, all values are written on a single line using the list delimiter
+         * as separator.
+         *
+         * @param key the property key
+         * @param value the property value
+         * @param forceSingleLine the &quot;force single line&quot; flag
+         * @throws IOException if an error occurs
+         * @since 1.3
+         */
+        public void writeProperty(final String key, final Object value,
+                final boolean forceSingleLine) throws IOException
         {
-            getCommentLines().clear();
-            final StringBuilder buffer = new StringBuilder();
+            String v;
 
-            while (true)
+            if (value instanceof List)
             {
-                String line = readLine();
-                if (line == null)
+                v = null;
+                final List<?> values = (List<?>) value;
+                if (forceSingleLine)
                 {
-                    // EOF
-                    if (buffer.length() > 0)
+                    try
                     {
-                        break;
+                        v = String.valueOf(getDelimiterHandler()
+                                        .escapeList(values, valueTransformer));
                     }
-                    return null;
-                }
-
-                // while a property line continues there are no comments (even if the line from
-                // the file looks like one)
-                if (isCommentLine(line) && (buffer.length() == 0))
-                {
-                    getCommentLines().add(line);
-                    continue;
-                }
-
-                // while property line continues left trim all following lines read from the
-                // file
-                if (buffer.length() > 0)
-                {
-                    // index of the first non-whitespace character
-                    int i;
-                    for (i = 0; i < line.length(); i++)
+                    catch (final UnsupportedOperationException uoex)
                     {
-                        if (!Character.isWhitespace(line.charAt(i)))
-                        {
-                            break;
-                        }
+                        // the handler may not support escaping lists,
+                        // then the list is written in multiple lines
                     }
-
-                    line = line.substring(i);
-                }
-
-                if (checkCombineLines(line))
-                {
-                    line = line.substring(0, line.length() - 1);
-                    buffer.append(line);
                 }
-                else
+                if (v == null)
                 {
-                    buffer.append(line);
-                    break;
+                    writeProperty(key, values);
+                    return;
                 }
             }
-            return buffer.toString();
-        }
+            else
+            {
+                v = String.valueOf(getDelimiterHandler().escape(value, valueTransformer));
+            }
 
-        @Override
-        protected void parseProperty(final String line)
-        {
-            final String[] property = doParseProperty(line, false);
-            initPropertyName(property[0]);
-            initPropertyValue(property[1]);
-            initPropertySeparator(property[2]);
+            write(escapeKey(key));
+            write(fetchSeparator(key, value));
+            write(v);
+
+            writeln(null);
         }
+    } // class PropertiesWriter
 
-        @Override
-        protected String unescapePropertyValue(final String value)
+    /**
+     * Defines default error handling for the special {@code "include"} key by throwing the given exception.
+     *
+     * @since 2.6
+     */
+    public static final ConfigurationConsumer<ConfigurationException> DEFAULT_INCLUDE_LISTENER = e -> { throw e; };
+
+    /**
+     * Defines error handling as a noop for the special {@code "include"} key.
+     *
+     * @since 2.6
+     */
+    public static final ConfigurationConsumer<ConfigurationException> NOOP_INCLUDE_LISTENER = e -> { /* noop */ };
+
+    /**
+     * The default encoding (ISO-8859-1 as specified by
+     * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
+     */
+    public static final String DEFAULT_ENCODING = "ISO-8859-1";
+
+    /** Constant for the supported comment characters.*/
+    static final String COMMENT_CHARS = "#!";
+
+    /** Constant for the default properties separator.*/
+    static final String DEFAULT_SEPARATOR = " = ";
+
+    /**
+     * A string with special characters that need to be unescaped when reading
+     * a properties file. {@code java.util.Properties} escapes these characters
+     * when writing out a properties file.
+     */
+    private static final String UNESCAPE_CHARACTERS = ":#=!\\\'\"";
+
+    /**
+     * This is the name of the property that can point to other
+     * properties file for including other properties files.
+     */
+    private static String include = "include";
+
+    /**
+     * This is the name of the property that can point to other
+     * properties file for including other properties files.
+     * <p>
+     * If the file is absent, processing continues normally.
+     * </p>
+     */
+    private static String includeOptional = "includeoptional";
+
+    /** The list of possible key/value separators */
+    private static final char[] SEPARATORS = new char[] {'=', ':'};
+
+    /** The white space characters used as key/value separators. */
+    private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'};
+
+    /** Constant for the platform specific line separator.*/
+    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
+    /** Constant for the radix of hex numbers.*/
+    private static final int HEX_RADIX = 16;
+
+    /** Constant for the length of a unicode literal.*/
+    private static final int UNICODE_LEN = 4;
+
+    /**
+     * Returns the number of trailing backslashes. This is sometimes needed for
+     * the correct handling of escape characters.
+     *
+     * @param line the string to investigate
+     * @return the number of trailing backslashes
+     */
+    private static int countTrailingBS(final String line)
+    {
+        int bsCount = 0;
+        for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
         {
-            return unescapeJava(value, true);
+            bsCount++;
         }
 
+        return bsCount;
+    }
+
+    /**
+     * Gets the property value for including other properties files.
+     * By default it is "include".
+     *
+     * @return A String.
+     */
+    public static String getInclude()
+    {
+        return PropertiesConfiguration.include;
+    }
+
+    /**
+     * Gets the property value for including other properties files.
+     * By default it is "includeoptional".
+     * <p>
+     * If the file is absent, processing continues normally.
+     * </p>
+     *
+     * @return A String.
+     * @since 2.5
+     */
+    public static String getIncludeOptional()
+    {
+        return PropertiesConfiguration.includeOptional;
+    }
+
+    /**
+     * Tests whether a line is a comment, i.e. whether it starts with a comment
+     * character.
+     *
+     * @param line the line
+     * @return a flag if this is a comment line
+     * @since 1.3
+     */
+    static boolean isCommentLine(final String line)
+    {
+        final String s = line.trim();
+        // blanc lines are also treated as comment lines
+        return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
     }
 
     /**
-     * A {@link PropertiesWriter} that tries to mimic the behavior of
-     * {@link java.util.Properties}.
+     * Checks whether the specified character needs to be unescaped. This method
+     * is called when during reading a property file an escape character ('\')
+     * is detected. If the character following the escape character is
+     * recognized as a special character which is escaped per default in a Java
+     * properties file, it has to be unescaped.
      *
-     * @since 2.4
+     * @param ch the character in question
+     * @return a flag whether this character has to be unescaped
      */
-    public static class JupPropertiesWriter extends PropertiesWriter
+    private static boolean needsUnescape(final char ch)
     {
+        return UNESCAPE_CHARACTERS.indexOf(ch) >= 0;
+    }
 
-        /**
-         * The starting ASCII printable character.
-         */
-        private static final int PRINTABLE_INDEX_END = 0x7e;
-
-        /**
-         * The ending ASCII printable character.
-         */
-        private static final int PRINTABLE_INDEX_START = 0x20;
-
-        /**
-         * A UnicodeEscaper for characters outside the ASCII printable range.
-         */
-        private static final UnicodeEscaper ESCAPER = UnicodeEscaper.outsideOf(PRINTABLE_INDEX_START,
-            PRINTABLE_INDEX_END);
-
-        /**
-         * Characters that need to be escaped when wring a properties file.
-         */
-        private static final Map<CharSequence, CharSequence> JUP_CHARS_ESCAPE;
-        static
-        {
-            final Map<CharSequence, CharSequence> initialMap = new HashMap<>();
-            initialMap.put("\\", "\\\\");
-            initialMap.put("\n", "\\n");
-            initialMap.put("\t", "\\t");
-            initialMap.put("\f", "\\f");
-            initialMap.put("\r", "\\r");
-            JUP_CHARS_ESCAPE = Collections.unmodifiableMap(initialMap);
-        }
-
-        /**
-         * Creates a new instance of {@code JupPropertiesWriter}.
-         *
-         * @param writer a Writer object providing the underlying stream
-         * @param delHandler the delimiter handler for dealing with properties with
-         *        multiple values
-         * @param escapeUnicode whether Unicode characters should be escaped using
-         *        Unicode escapes
-         */
-        public JupPropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler,
-            final boolean escapeUnicode)
-        {
-            super(writer, delHandler, value -> {
-                String valueString = String.valueOf(value);
-
-                CharSequenceTranslator translator;
-                if (escapeUnicode)
-                {
-                    translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE), ESCAPER);
-                }
-                else
-                {
-                    translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE));
-                }
-
-                valueString = translator.translate(valueString);
-
-                // escape the first leading space to preserve it (and all after it)
-                if (valueString.startsWith(" "))
-                {
-                    valueString = "\\" + valueString;
-                }
-
-                return valueString;
-            });
-        }
+    /**
+     * Sets the property value for including other properties files.
+     * By default it is "include".
+     *
+     * @param inc A String.
+     */
+    public static void setInclude(final String inc)
+    {
+        PropertiesConfiguration.include = inc;
+    }
 
+    /**
+     * Sets the property value for including other properties files.
+     * By default it is "include".
+     * <p>
+     * If the file is absent, processing continues normally.
+     * </p>
+     *
+     * @param inc A String.
+     * @since 2.5
+     */
+    public static void setIncludeOptional(final String inc)
+    {
+        PropertiesConfiguration.includeOptional = inc;
     }
 
     /**
@@ -1795,39 +1443,199 @@ public class PropertiesConfiguration extends BaseConfiguration
                     out.append(ch);
                 }
 
-                continue;
-            }
-            else if (ch == '\\')
-            {
-                hadSlash = true;
-                continue;
-            }
-            out.append(ch);
-        }
+                continue;
+            }
+            else if (ch == '\\')
+            {
+                hadSlash = true;
+                continue;
+            }
+            out.append(ch);
+        }
+
+        if (hadSlash)
+        {
+            // then we're in the weird case of a \ at the end of the
+            // string, let's output it anyway.
+            out.append('\\');
+        }
+
+        return out.toString();
+    }
+
+    /** Stores the layout object.*/
+    private PropertiesConfigurationLayout layout;
+
+    /** The include listener for the special {@code "include"} key. */
+    private ConfigurationConsumer<ConfigurationException> includeListener;
+
+    /** The IOFactory for creating readers and writers.*/
+    private IOFactory ioFactory;
+
+    /** The current {@code FileLocator}. */
+    private FileLocator locator;
+
+    /** Allow file inclusion or not */
+    private boolean includesAllowed = true;
+
+    /**
+     * Creates an empty PropertyConfiguration object which can be
+     * used to synthesize a new Properties file by adding values and
+     * then saving().
+     */
+    public PropertiesConfiguration()
+    {
+        installLayout(createLayout());
+    }
+
+    /**
+     * Creates a copy of this object.
+     *
+     * @return the copy
+     */
+    @Override
+    public Object clone()
+    {
+        final PropertiesConfiguration copy = (PropertiesConfiguration) super.clone();
+        if (layout != null)
+        {
+            copy.setLayout(new PropertiesConfigurationLayout(layout));
+        }
+        return copy;
+    }
+
+    /**
+     * Creates a standard layout object. This configuration is initialized with
+     * such a standard layout.
+     *
+     * @return the newly created layout object
+     */
+    private PropertiesConfigurationLayout createLayout()
+    {
+        return new PropertiesConfigurationLayout();
+    }
+
+    /**
+     * Returns the footer comment. This is a comment at the very end of the
+     * file.
+     *
+     * @return the footer comment
+     * @since 2.0
+     */
+    public String getFooter()
+    {
+        beginRead(false);
+        try
+        {
+            return getLayout().getFooterComment();
+        }
+        finally
+        {
+            endRead();
+        }
+    }
+
+    /**
+     * Return the comment header.
+     *
+     * @return the comment header
+     * @since 1.1
+     */
+    public String getHeader()
+    {
+        beginRead(false);
+        try
+        {
+            return getLayout().getHeaderComment();
+        }
+        finally
+        {
+            endRead();
+        }
+    }
+
+    /**
+     * Gets the current include listener, never null.
+     *
+     * @return the current include listener, never null.
+     * @since 2.6
+     */
+    public ConfigurationConsumer<ConfigurationException> getIncludeListener()
+    {
+        return includeListener != null ? includeListener : PropertiesConfiguration.DEFAULT_INCLUDE_LISTENER;
+    }
+
+    /**
+     * Returns the {@code IOFactory} to be used for creating readers and
+     * writers when loading or saving this configuration.
+     *
+     * @return the {@code IOFactory}
+     * @since 1.7
+     */
+    public IOFactory getIOFactory()
+    {
+        return ioFactory != null ? ioFactory : DefaultIOFactory.INSTANCE;
+    }
+
+    /**
+     * Returns the associated layout object.
+     *
+     * @return the associated layout object
+     * @since 1.3
+     */
+    public PropertiesConfigurationLayout getLayout()
+    {
+        return layout;
+    }
+
+    /**
+     * Stores the current {@code FileLocator} for a following IO operation. The
+     * {@code FileLocator} is needed to resolve include files with relative file
+     * names.
+     *
+     * @param locator the current {@code FileLocator}
+     * @since 2.0
+     */
+    @Override
+    public void initFileLocator(final FileLocator locator)
+    {
+        this.locator = locator;
+    }
 
-        if (hadSlash)
+    /**
+     * Installs a layout object. It has to be ensured that the layout is
+     * registered as change listener at this configuration. If there is already
+     * a layout object installed, it has to be removed properly.
+     *
+     * @param layout the layout object to be installed
+     */
+    private void installLayout(final PropertiesConfigurationLayout layout)
+    {
+        // only one layout must exist
+        if (this.layout != null)
         {
-            // then we're in the weird case of a \ at the end of the
-            // string, let's output it anyway.
-            out.append('\\');
+            removeEventListener(ConfigurationEvent.ANY, this.layout);
         }
 
-        return out.toString();
+        if (layout == null)
+        {
+            this.layout = createLayout();
+        }
+        else
+        {
+            this.layout = layout;
+        }
+        addEventListener(ConfigurationEvent.ANY, this.layout);
     }
 
     /**
-     * Checks whether the specified character needs to be unescaped. This method
-     * is called when during reading a property file an escape character ('\')
-     * is detected. If the character following the escape character is
-     * recognized as a special character which is escaped per default in a Java
-     * properties file, it has to be unescaped.
+     * Reports the status of file inclusion.
      *
-     * @param ch the character in question
-     * @return a flag whether this character has to be unescaped
+     * @return True if include files are loaded.
      */
-    private static boolean needsUnescape(final char ch)
+    public boolean isIncludesAllowed()
     {
-        return UNESCAPE_CHARACTERS.indexOf(ch) >= 0;
+        return this.includesAllowed;
     }
 
     /**
@@ -1926,4 +1734,196 @@ public class PropertiesConfiguration extends BaseConfiguration
         return FileLocatorUtils.locate(includeLocator);
     }
 
+    /**
+     * This method is invoked by the associated
+     * {@link PropertiesConfigurationLayout} object for each
+     * property definition detected in the parsed properties file. Its task is
+     * to check whether this is a special property definition (e.g. the
+     * {@code include} property). If not, the property must be added to
+     * this configuration. The return value indicates whether the property
+     * should be treated as a normal property. If it is <b>false</b>, the
+     * layout object will ignore this property.
+     *
+     * @param key the property key
+     * @param value the property value
+     * @param seenStack the stack of seen include URLs
+     * @return a flag whether this is a normal property
+     * @throws ConfigurationException if an error occurs
+     * @since 1.3
+     */
+    boolean propertyLoaded(final String key, final String value, final Deque<URL> seenStack)
+            throws ConfigurationException
+    {
+        boolean result;
+
+        if (StringUtils.isNotEmpty(getInclude())
+                && key.equalsIgnoreCase(getInclude()))
+        {
+            if (isIncludesAllowed())
+            {
+                final Collection<String> files =
+                        getListDelimiterHandler().split(value, true);
+                for (final String f : files)
+                {
+                    loadIncludeFile(interpolate(f), false, seenStack);
+                }
+            }
+            result = false;
+        }
+
+        else if (StringUtils.isNotEmpty(getIncludeOptional())
+            && key.equalsIgnoreCase(getIncludeOptional()))
+        {
+            if (isIncludesAllowed())
+            {
+                final Collection<String> files =
+                        getListDelimiterHandler().split(value, true);
+                for (final String f : files)
+                {
+                    loadIncludeFile(interpolate(f), true, seenStack);
+                }
+            }
+            result = false;
+        }
+
+        else
+        {
+            addPropertyInternal(key, value);
+            result = true;
+        }
+
+        return result;
+    }
+
+    /**
+     * {@inheritDoc} This implementation delegates to the associated layout
+     * object which does the actual loading. Note that this method does not
+     * do any synchronization. This lies in the responsibility of the caller.
+     * (Typically, the caller is a {@code FileHandler} object which takes
+     * care for proper synchronization.)
+     *
+     * @since 2.0
+     */
+    @Override
+    public void read(final Reader in) throws ConfigurationException, IOException
+    {
+        getLayout().load(this, in);
+    }
+
+    /**
+     * Sets the footer comment. If set, this comment is written after all
+     * properties at the end of the file.
+     *
+     * @param footer the footer comment
+     * @since 2.0
+     */
+    public void setFooter(final String footer)
+    {
+        beginWrite(false);
+        try
+        {
+            getLayout().setFooterComment(footer);
+        }
+        finally
+        {
+            endWrite();
+        }
+    }
+
+    /**
+     * Set the comment header.
+     *
+     * @param header the header to use
+     * @since 1.1
+     */
+    public void setHeader(final String header)
+    {
+        beginWrite(false);
+        try
+        {
+            getLayout().setHeaderComment(header);
+        }
+        finally
+        {
+            endWrite();
+        }
+    }
+
+    /**
+     * Sets the current include listener, may not be null.
+     *
+     * @param includeListener the current include listener, may not be null.
+     * @throws IllegalArgumentException if the {@code includeListener} is null.
+     * @since 2.6
+     */
+    public void setIncludeListener(final ConfigurationConsumer<ConfigurationException> includeListener)
+    {
+        if (includeListener == null)
+        {
+            throw new IllegalArgumentException("includeListener must not be null.");
+        }
+        this.includeListener = includeListener;
+    }
+
+    /**
+     * Controls whether additional files can be loaded by the {@code include = <xxx>}
+     * statement or not. This is <b>true</b> per default.
+     *
+     * @param includesAllowed True if Includes are allowed.
+     */
+    public void setIncludesAllowed(final boolean includesAllowed)
+    {
+        this.includesAllowed = includesAllowed;
+    }
+
+    /**
+     * Sets the {@code IOFactory} to be used for creating readers and
+     * writers when loading or saving this configuration. Using this method a
+     * client can customize the reader and writer classes used by the load and
+     * save operations. Note that this method must be called before invoking
+     * one of the {@code load()} and {@code save()} methods.
+     * Especially, if you want to use a custom {@code IOFactory} for
+     * changing the {@code PropertiesReader}, you cannot load the
+     * configuration data in the constructor.
+     *
+     * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>)
+     * @throws IllegalArgumentException if the {@code IOFactory} is
+     *         <b>null</b>
+     * @since 1.7
+     */
+    public void setIOFactory(final IOFactory ioFactory)
+    {
+        if (ioFactory == null)
+        {
+            throw new IllegalArgumentException("IOFactory must not be null.");
+        }
+
+        this.ioFactory = ioFactory;
+    }
+
+    /**
+     * Sets the associated layout object.
+     *
+     * @param layout the new layout object; can be <b>null</b>, then a new
+     * layout object will be created
+     * @since 1.3
+     */
+    public void setLayout(final PropertiesConfigurationLayout layout)
+    {
+        installLayout(layout);
+    }
+
+    /**
+     * {@inheritDoc} This implementation delegates to the associated layout
+     * object which does the actual saving. Note that, analogous to
+     * {@link #read(Reader)}, this method does not do any synchronization.
+     *
+     * @since 2.0
+     */
+    @Override
+    public void write(final Writer out) throws ConfigurationException, IOException
+    {
+        getLayout().save(this, out);
+    }
+
 }