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 2019/09/12 14:52:17 UTC

[commons-configuration] branch master updated: [CONFIGURATION-756] Allow for custom behavior to handle errors loading included properties files. (#34)

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


The following commit(s) were added to refs/heads/master by this push:
     new 8b31196  [CONFIGURATION-756] Allow for custom behavior to handle errors loading included properties files. (#34)
8b31196 is described below

commit 8b311965eb0c04508461a3e14169f79a1c065678
Author: Gary Gregory <ga...@users.noreply.github.com>
AuthorDate: Thu Sep 12 10:52:13 2019 -0400

    [CONFIGURATION-756] Allow for custom behavior to handle errors loading included properties files. (#34)
    
    [CONFIGURATION-756] Allow for custom behavior to handle errors loading included properties files. (#34)
---
 .../configuration2/ConfigurationConsumer.java      |  39 +++++++
 .../configuration2/PropertiesConfiguration.java    | 112 +++++++++++++++++----
 .../builder/PropertiesBuilderParametersImpl.java   |  15 ++-
 .../builder/PropertiesBuilderProperties.java       |  14 +++
 .../builder/fluent/Configurations.java             |  44 +++++++-
 .../TestPropertiesConfiguration.java               |  82 ++++++++++++++-
 .../TestPropertiesBuilderParametersImpl.java       |  43 +++++++-
 .../combined/TestCombinedConfigurationBuilder.java |   2 +-
 .../builder/fluent/TestConfigurations.java         | 105 ++++++++++++++++---
 .../builder/fluent/TestParameters.java             |  17 +++-
 .../include-cyclical-reference.properties          |  16 +++
 .../resources/include-include-not-found.properties |  16 +++
 .../resources/include-load-exception.properties    |  16 +++
 src/test/resources/include-not-found.properties    |  16 +++
 14 files changed, 489 insertions(+), 48 deletions(-)

diff --git a/src/main/java/org/apache/commons/configuration2/ConfigurationConsumer.java b/src/main/java/org/apache/commons/configuration2/ConfigurationConsumer.java
new file mode 100644
index 0000000..3e96418
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration2/ConfigurationConsumer.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration2;
+
+import org.apache.commons.configuration2.ex.ConfigurationException;
+
+/**
+ * A Configuration task that may throw a ConfigurationException.
+ *
+ * @param <T> the type of the input to the operation.
+ * @since 2.6
+ */
+@FunctionalInterface
+public interface ConfigurationConsumer<T>
+{
+
+    /**
+     * Performs this operation on the given argument.
+     *
+     * @param t the input argument
+     * @throws ConfigurationException May be thrown while performing a Configuration operation.
+     */
+    void accept(T t) throws ConfigurationException;
+}
diff --git a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
index 3b86bbe..438bba8 100644
--- a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
+++ b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
@@ -17,6 +17,7 @@
 
 package org.apache.commons.configuration2;
 
+import java.io.FileNotFoundException;
 import java.io.FilterWriter;
 import java.io.IOException;
 import java.io.LineNumberReader;
@@ -137,6 +138,10 @@ import org.apache.commons.text.translate.UnicodeEscaper;
  *   they do not replace existing properties with the same key.
  *
  *  </li>
+ *  <li>
+ *   You can define custom error handling for the special key {@code "include"}
+ *   by using {@link #setIncludeListener(ConfigurationConsumer)}.
+ *  </li>
  * </ul>
  *
  * <p>Here is an example of a valid extended properties file:</p>
@@ -194,11 +199,31 @@ import org.apache.commons.text.translate.UnicodeEscaper;
  * Properties files</a> in special.
  *
  * @see java.util.Properties#load
- *
  */
 public class PropertiesConfiguration extends BaseConfiguration
     implements FileBasedConfiguration, FileLocatorAware
 {
+
+    /**
+     * 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)
@@ -212,12 +237,6 @@ public class PropertiesConfiguration extends BaseConfiguration
     static final String DEFAULT_SEPARATOR = " = ";
 
     /**
-     * Constant for the default {@code IOFactory}. This instance is used
-     * when no specific factory was set.
-     */
-    private static final IOFactory DEFAULT_IO_FACTORY = new DefaultIOFactory();
-
-    /**
      * 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.
@@ -257,6 +276,9 @@ public class PropertiesConfiguration extends BaseConfiguration
     /** 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;
 
@@ -488,6 +510,17 @@ public class PropertiesConfiguration extends BaseConfiguration
     }
 
     /**
+     * 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.
      *
@@ -496,7 +529,23 @@ public class PropertiesConfiguration extends BaseConfiguration
      */
     public IOFactory getIOFactory()
     {
-        return (ioFactory != null) ? ioFactory : DEFAULT_IO_FACTORY;
+        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)
+        {
+            throw new IllegalArgumentException("includeListener must not be null.");
+        }
+        this.includeListener = includeListener;
     }
 
     /**
@@ -518,7 +567,7 @@ public class PropertiesConfiguration extends BaseConfiguration
     {
         if (ioFactory == null)
         {
-            throw new IllegalArgumentException("IOFactory must not be null!");
+            throw new IllegalArgumentException("IOFactory must not be null.");
         }
 
         this.ioFactory = ioFactory;
@@ -1377,6 +1426,11 @@ public class PropertiesConfiguration extends BaseConfiguration
      */
     public static class DefaultIOFactory implements IOFactory
     {
+        /**
+         * The singleton instance.
+         */
+        static final DefaultIOFactory INSTANCE = new DefaultIOFactory();
+
         @Override
         public PropertiesReader createPropertiesReader(final Reader in)
         {
@@ -1827,20 +1881,35 @@ public class PropertiesConfiguration extends BaseConfiguration
 
         if (url == null)
         {
-            throw new ConfigurationException("Cannot resolve include file "
-                    + fileName);
-        }
-
-        final FileHandler fh = new FileHandler(this);
-        fh.setFileLocator(locator);
-        final FileLocator orgLocator = locator;
-        try
-        {
-            fh.load(url);
+            if (getIncludeListener() != null)
+            {
+                getIncludeListener().accept(new ConfigurationException(
+                        "Cannot resolve include file " + fileName, new FileNotFoundException(fileName)));
+            }
         }
-        finally
+        else
         {
-            locator = orgLocator; // reset locator which is changed by load
+            final FileHandler fh = new FileHandler(this);
+            fh.setFileLocator(locator);
+            final FileLocator orgLocator = locator;
+            try
+            {
+                try
+                {
+                    fh.load(url);
+                }
+                catch (ConfigurationException e)
+                {
+                    if (getIncludeListener() != null)
+                    {
+                        getIncludeListener().accept(e);
+                    }
+                }
+            }
+            finally
+            {
+                locator = orgLocator; // reset locator which is changed by load
+            }
         }
     }
 
@@ -1860,4 +1929,5 @@ public class PropertiesConfiguration extends BaseConfiguration
                         .basePath(basePath).fileName(fileName).create();
         return FileLocatorUtils.locate(includeLocator);
     }
+
 }
diff --git a/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderParametersImpl.java b/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderParametersImpl.java
index a908ac0..39965a7 100644
--- a/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderParametersImpl.java
+++ b/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderParametersImpl.java
@@ -18,8 +18,10 @@ package org.apache.commons.configuration2.builder;
 
 import java.util.Map;
 
+import org.apache.commons.configuration2.ConfigurationConsumer;
 import org.apache.commons.configuration2.PropertiesConfiguration.IOFactory;
 import org.apache.commons.configuration2.PropertiesConfigurationLayout;
+import org.apache.commons.configuration2.ex.ConfigurationException;
 
 /**
  * <p>
@@ -44,6 +46,9 @@ public class PropertiesBuilderParametersImpl extends
         FileBasedBuilderParametersImpl implements
         PropertiesBuilderProperties<PropertiesBuilderParametersImpl>
 {
+    /** The key for the include listener property. */
+    private static final String PROP_INCLUDE_LISTENER = "includeListener";
+
     /** The key for the includes allowed property. */
     private static final String PROP_INCLUDES_ALLOWED = "includesAllowed";
 
@@ -54,6 +59,14 @@ public class PropertiesBuilderParametersImpl extends
     private static final String PROP_IO_FACTORY = "IOFactory";
 
     @Override
+    public PropertiesBuilderParametersImpl setIncludeListener(
+            final ConfigurationConsumer<ConfigurationException> includeListener)
+    {
+        storeProperty(PROP_INCLUDE_LISTENER, includeListener);
+        return this;
+    }
+
+    @Override
     public PropertiesBuilderParametersImpl setIncludesAllowed(final boolean f)
     {
         storeProperty(PROP_INCLUDES_ALLOWED, Boolean.valueOf(f));
@@ -68,7 +81,7 @@ public class PropertiesBuilderParametersImpl extends
     public void inheritFrom(final Map<String, ?> source)
     {
         super.inheritFrom(source);
-        copyPropertiesFrom(source, PROP_INCLUDES_ALLOWED, PROP_IO_FACTORY);
+        copyPropertiesFrom(source, PROP_INCLUDES_ALLOWED, PROP_INCLUDE_LISTENER, PROP_IO_FACTORY);
     }
 
     @Override
diff --git a/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderProperties.java b/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderProperties.java
index 939339b..62040ed 100644
--- a/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderProperties.java
+++ b/src/main/java/org/apache/commons/configuration2/builder/PropertiesBuilderProperties.java
@@ -16,8 +16,10 @@
  */
 package org.apache.commons.configuration2.builder;
 
+import org.apache.commons.configuration2.ConfigurationConsumer;
 import org.apache.commons.configuration2.PropertiesConfiguration.IOFactory;
 import org.apache.commons.configuration2.PropertiesConfigurationLayout;
+import org.apache.commons.configuration2.ex.ConfigurationException;
 
 /**
  * <p>
@@ -39,6 +41,18 @@ import org.apache.commons.configuration2.PropertiesConfigurationLayout;
 public interface PropertiesBuilderProperties<T>
 {
     /**
+     * Sets the current include listener, may be null.
+     *
+     * @param includeListener the current include listener, may be null.
+     * @return a reference to this object for method chaining
+     * @since 2.6
+     */
+    default T setIncludeListener(ConfigurationConsumer<ConfigurationException> includeListener)
+    {
+        return (T) this;
+    }
+
+    /**
      * Sets a flag whether include files are supported by the properties
      * configuration object. If set to <b>true</b>, files listed by an include
      * property are loaded automatically.
diff --git a/src/main/java/org/apache/commons/configuration2/builder/fluent/Configurations.java b/src/main/java/org/apache/commons/configuration2/builder/fluent/Configurations.java
index 55f6ba5..5c07d71 100644
--- a/src/main/java/org/apache/commons/configuration2/builder/fluent/Configurations.java
+++ b/src/main/java/org/apache/commons/configuration2/builder/fluent/Configurations.java
@@ -225,6 +225,17 @@ public class Configurations
     }
 
     /**
+     * Creates a builder for a {@code PropertiesConfiguration}.
+     *
+     * @return the newly created {@code FileBasedConfigurationBuilder}
+     * @since 2.6
+     */
+    public FileBasedConfigurationBuilder<PropertiesConfiguration> propertiesBuilder()
+    {
+        return createFileBasedBuilder(PropertiesConfiguration.class);
+    }
+
+    /**
      * Creates a builder for a {@code PropertiesConfiguration} and initializes
      * it with the given file to be loaded.
      *
@@ -239,6 +250,20 @@ public class Configurations
 
     /**
      * Creates a builder for a {@code PropertiesConfiguration} and initializes
+     * it with the given parameters to be loaded.
+     *
+     * @param parameters the parameters to be loaded
+     * @return the newly created {@code FileBasedConfigurationBuilder}
+     * @since 2.6
+     */
+    public FileBasedConfigurationBuilder<PropertiesConfiguration> propertiesBuilder(
+            PropertiesBuilderParameters parameters)
+    {
+        return propertiesBuilder().configure(parameters);
+    }
+
+    /**
+     * Creates a builder for a {@code PropertiesConfiguration} and initializes
      * it with the given URL to be loaded.
      *
      * @param url the URL to be loaded
@@ -579,6 +604,21 @@ public class Configurations
      * specified type.
      *
      * @param configClass the configuration class
+     * @param <T> the type of the configuration to be constructed
+     * @return the newly created builder
+     * @since 2.6
+     */
+    private <T extends FileBasedConfiguration> FileBasedConfigurationBuilder<T> createFileBasedBuilder(
+            final Class<T> configClass)
+    {
+        return new FileBasedConfigurationBuilder<>(configClass);
+    }
+
+    /**
+     * Creates a configured builder for a file-based configuration of the
+     * specified type.
+     *
+     * @param configClass the configuration class
      * @param params the parameters object for configuring the builder
      * @param <T> the type of the configuration to be constructed
      * @return the newly created builder
@@ -586,8 +626,7 @@ public class Configurations
     private <T extends FileBasedConfiguration> FileBasedConfigurationBuilder<T> createFileBasedBuilder(
             final Class<T> configClass, final FileBasedBuilderParameters params)
     {
-        return new FileBasedConfigurationBuilder<>(configClass)
-                .configure(params);
+        return createFileBasedBuilder(configClass).configure(params);
     }
 
     /**
@@ -636,4 +675,5 @@ public class Configurations
     {
         return fileParams().setFileName(path);
     }
+
 }
diff --git a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
index dd63ff5..bd1e532 100644
--- a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
+++ b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
@@ -17,7 +17,31 @@
 
 package org.apache.commons.configuration2;
 
-import java.io.*;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLConnection;
@@ -50,13 +74,11 @@ import org.apache.commons.configuration2.io.FileHandler;
 import org.apache.commons.configuration2.io.FileSystem;
 import org.apache.commons.lang3.mutable.MutableObject;
 import org.junit.Before;
-import org.junit.Test;
+import org.junit.Ignore;
 import org.junit.Rule;
+import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.junit.Assert.*;
-
 /**
  * Test for loading and saving properties files.
  *
@@ -1088,6 +1110,56 @@ public class TestPropertiesConfiguration
     }
 
     @Test
+    public void testIncludeLoadAllOnNotFound() throws Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-not-found.properties");
+        handler.load();
+        assertEquals("valueA", pc.getString("keyA"));
+    }
+
+    @Test
+    public void testIncludeIncludeLoadAllOnNotFound() throws Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-include-not-found.properties");
+        handler.load();
+        assertEquals("valueA", pc.getString("keyA"));
+        assertEquals("valueB", pc.getString("keyB"));
+    }
+
+    @Test
+    public void testIncludeLoadAllOnLoadException() throws Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-load-exception.properties");
+        handler.load();
+        assertEquals("valueA", pc.getString("keyA"));
+    }
+
+    @Test
+    @Ignore("PropertiesConfiguration does NOT detect cyclical references.")
+    public void testIncludeLoadAllCycliclaReference() throws Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-cyclical-reference.properties");
+        handler.load();
+        assertEquals("valueA", pc.getString("keyA"));
+    }
+
+    @Test
     public void testLoadViaPropertyWithBasePath2() throws Exception
     {
         final PropertiesConfiguration pc = new PropertiesConfiguration();
diff --git a/src/test/java/org/apache/commons/configuration2/builder/TestPropertiesBuilderParametersImpl.java b/src/test/java/org/apache/commons/configuration2/builder/TestPropertiesBuilderParametersImpl.java
index 4606338..5815003 100644
--- a/src/test/java/org/apache/commons/configuration2/builder/TestPropertiesBuilderParametersImpl.java
+++ b/src/test/java/org/apache/commons/configuration2/builder/TestPropertiesBuilderParametersImpl.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertSame;
 
 import java.util.Map;
 
+import org.apache.commons.configuration2.ConfigurationConsumer;
 import org.apache.commons.configuration2.PropertiesConfiguration;
 import org.apache.commons.configuration2.PropertiesConfigurationLayout;
 import org.apache.commons.configuration2.beanutils.BeanHelper;
@@ -70,6 +71,20 @@ public class TestPropertiesBuilderParametersImpl
     }
 
     /**
+     * Tests whether the include listener can be set.
+     */
+    @Test
+    public void testSetIncludeListener()
+    {
+        final ConfigurationConsumer<ConfigurationException> includeListener =
+                EasyMock.createMock(ConfigurationConsumer.class);
+        EasyMock.replay(includeListener);
+        assertSame("Wrong result", params, params.setIncludeListener(includeListener));
+        assertSame("IncludeListener not set", includeListener,
+                params.getParameters().get("includeListener"));
+    }
+
+    /**
      * Tests whether the IO factory can be set.
      */
     @Test
@@ -113,9 +128,13 @@ public class TestPropertiesBuilderParametersImpl
     {
         final PropertiesConfiguration.IOFactory factory =
                 EasyMock.createMock(PropertiesConfiguration.IOFactory.class);
-        params.setIOFactory(factory).setIncludesAllowed(false)
-                .setLayout(new PropertiesConfigurationLayout());
-        params.setThrowExceptionOnMissing(true);
+        final ConfigurationConsumer<ConfigurationException> includeListener =
+                EasyMock.createMock(ConfigurationConsumer.class);
+        params.setIOFactory(factory)
+                .setIncludeListener(includeListener)
+                .setIncludesAllowed(false)
+                .setLayout(new PropertiesConfigurationLayout())
+                .setThrowExceptionOnMissing(true);
         final PropertiesBuilderParametersImpl params2 =
                 new PropertiesBuilderParametersImpl();
 
@@ -123,6 +142,7 @@ public class TestPropertiesBuilderParametersImpl
         final Map<String, Object> parameters = params2.getParameters();
         assertEquals("Exception flag not set", Boolean.TRUE,
                 parameters.get("throwExceptionOnMissing"));
+        assertEquals("IncludeListener not set", includeListener, parameters.get("includeListener"));
         assertEquals("IOFactory not set", factory, parameters.get("IOFactory"));
         assertEquals("Include flag not set", Boolean.FALSE,
                 parameters.get("includesAllowed"));
@@ -146,4 +166,21 @@ public class TestPropertiesBuilderParametersImpl
         final PropertiesConfiguration config = builder.getConfiguration();
         assertEquals("Wrong IO factory", factory, config.getIOFactory());
     }
+
+    /**
+     * Tests whether the IncludeListener property can be correctly set.
+     */
+    @Test
+    public void testSetIncludeListenerProperty() throws ConfigurationException
+    {
+        final ConfigurationConsumer<ConfigurationException> includeListener =
+                PropertiesConfiguration.DEFAULT_INCLUDE_LISTENER;
+        final ConfigurationBuilder<PropertiesConfiguration> builder =
+                new FileBasedConfigurationBuilder<>(
+                        PropertiesConfiguration.class)
+                .configure(params.setIncludeListener(includeListener));
+
+        final PropertiesConfiguration config = builder.getConfiguration();
+        assertEquals("Wrong IncludeListener", includeListener, config.getIncludeListener());
+    }
 }
diff --git a/src/test/java/org/apache/commons/configuration2/builder/combined/TestCombinedConfigurationBuilder.java b/src/test/java/org/apache/commons/configuration2/builder/combined/TestCombinedConfigurationBuilder.java
index 1017c21..0a59ea6 100644
--- a/src/test/java/org/apache/commons/configuration2/builder/combined/TestCombinedConfigurationBuilder.java
+++ b/src/test/java/org/apache/commons/configuration2/builder/combined/TestCombinedConfigurationBuilder.java
@@ -704,7 +704,7 @@ public class TestCombinedConfigurationBuilder
         final CombinedConfiguration cc = builder.getConfiguration();
         assertFalse("Configuration is empty", cc.isEmpty());
 
-        // The environment may contain settings with values that 
+        // The environment may contain settings with values that
         // are altered by interpolation. Disable this for direct access
         // to the String associated with the environment property name.
         cc.setInterpolator(null);
diff --git a/src/test/java/org/apache/commons/configuration2/builder/fluent/TestConfigurations.java b/src/test/java/org/apache/commons/configuration2/builder/fluent/TestConfigurations.java
index 8ac5e71..d42c1a1 100644
--- a/src/test/java/org/apache/commons/configuration2/builder/fluent/TestConfigurations.java
+++ b/src/test/java/org/apache/commons/configuration2/builder/fluent/TestConfigurations.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertSame;
 
 import java.io.File;
 import java.net.URL;
+import java.util.Map;
 
 import org.apache.commons.configuration2.CombinedConfiguration;
 import org.apache.commons.configuration2.Configuration;
@@ -29,10 +30,12 @@ import org.apache.commons.configuration2.ConfigurationAssert;
 import org.apache.commons.configuration2.INIConfiguration;
 import org.apache.commons.configuration2.PropertiesConfiguration;
 import org.apache.commons.configuration2.XMLConfiguration;
+import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
 import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
 import org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder;
 import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.plist.PropertyListConfiguration;
+import org.junit.Assert;
 import org.junit.Test;
 
 /**
@@ -57,12 +60,12 @@ public class TestConfigurations
     private static final String TEST_PLIST = "test.plist";
 
     /**
-     * Generates a full path for the test file with the given name.
+     * Generates an absolute path for the test file with the given name.
      *
      * @param name the name of the test file
      * @return the full path to this file
      */
-    private static String filePath(final String name)
+    private static String absolutePath(final String name)
     {
         return ConfigurationAssert.getTestFile(name).getAbsolutePath();
     }
@@ -126,7 +129,7 @@ public class TestConfigurations
     public void testFileBasedBuilderWithPath()
     {
         final Configurations configs = new Configurations();
-        final String filePath = filePath(TEST_PROPERTIES);
+        final String filePath = absolutePath(TEST_PROPERTIES);
         final FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
                 configs.fileBasedBuilder(PropertiesConfiguration.class,
                         filePath);
@@ -179,7 +182,7 @@ public class TestConfigurations
         final Configurations configs = new Configurations();
         final PropertyListConfiguration config =
                 configs.fileBased(PropertyListConfiguration.class,
-                        filePath(TEST_PLIST));
+                        absolutePath(TEST_PLIST));
         checkPList(config);
     }
 
@@ -256,11 +259,89 @@ public class TestConfigurations
     {
         final Configurations configs = new Configurations();
         final FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
-                configs.propertiesBuilder(filePath(TEST_PROPERTIES));
+                configs.propertiesBuilder(absolutePath(TEST_PROPERTIES));
         checkProperties(builder.getConfiguration());
     }
 
     /**
+     * Tests whether a builder for a properties configuration can be created for
+     * a given file path when an include is not found.
+     */
+    @Test
+    public void testPropertiesBuilderFromPathIncludeNotFoundFail() throws ConfigurationException
+    {
+        final Configurations configs = new Configurations();
+        final FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
+                configs.propertiesBuilder(absolutePath("include-not-found.properties"));
+        try
+        {
+            builder.getConfiguration();
+            Assert.fail("Expected ConfigurationException");
+        }
+        catch (ConfigurationException e) {
+            // ignore
+        }
+    }
+
+    /**
+     * Tests whether a builder for a properties configuration can be created for
+     * a given file path when an include is not found.
+     */
+    @Test
+    public void testPropertiesBuilderFromPathIncludeNotFoundPass() throws ConfigurationException
+    {
+        final Configurations configs = new Configurations();
+        final String absPath = absolutePath("include-not-found.properties");
+        final FileBasedConfigurationBuilder<PropertiesConfiguration> builderFail =
+                configs.propertiesBuilder(absPath);
+        // Expect failure:
+        try
+        {
+            builderFail.getConfiguration();
+            Assert.fail("Expected ConfigurationException");
+        }
+        catch (ConfigurationException e)
+        {
+            // Ignore
+            // e.printStackTrace();
+        }
+        // Expect failure:
+        try
+        {
+            configs.properties(absPath);
+        }
+        catch (ConfigurationException e) {
+            // Ignore
+            // e.printStackTrace();
+        }
+        {
+            // Expect success:
+            // @formatter:off
+            final Map<String, Object> map =
+                    new Parameters().properties()
+                            .setPath(absPath)
+                            .setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER)
+                            .getParameters();
+            // @formatter:on
+            final BasicConfigurationBuilder<PropertiesConfiguration> builderOk = configs.propertiesBuilder(absPath)
+                    .addParameters(map);
+            final PropertiesConfiguration configuration = builderOk.getConfiguration();
+            assertEquals("valueA", configuration.getString("keyA"));
+        }
+        {
+            // Expect success:
+            // @formatter:off
+            final BasicConfigurationBuilder<PropertiesConfiguration> builderOk = configs.propertiesBuilder(
+                    new Parameters().properties()
+                        .setPath(absPath)
+                        .setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER));
+            // @formatter:on
+            final PropertiesConfiguration configuration = builderOk.getConfiguration();
+            assertEquals("valueA", configuration.getString("keyA"));
+        }
+    }
+
+    /**
      * Tests whether a properties configuration can be loaded from a file path.
      */
     @Test
@@ -268,7 +349,7 @@ public class TestConfigurations
     {
         final Configurations configs = new Configurations();
         final PropertiesConfiguration config =
-                configs.properties(filePath(TEST_PROPERTIES));
+                configs.properties(absolutePath(TEST_PROPERTIES));
         checkProperties(config);
     }
 
@@ -341,7 +422,7 @@ public class TestConfigurations
     {
         final Configurations configs = new Configurations();
         final FileBasedConfigurationBuilder<XMLConfiguration> builder =
-                configs.xmlBuilder(filePath(TEST_XML));
+                configs.xmlBuilder(absolutePath(TEST_XML));
         checkXML(builder.getConfiguration());
     }
 
@@ -352,7 +433,7 @@ public class TestConfigurations
     public void testXMLFromPath() throws ConfigurationException
     {
         final Configurations configs = new Configurations();
-        final XMLConfiguration config = configs.xml(filePath(TEST_XML));
+        final XMLConfiguration config = configs.xml(absolutePath(TEST_XML));
         checkXML(config);
     }
 
@@ -425,7 +506,7 @@ public class TestConfigurations
     {
         final Configurations configs = new Configurations();
         final FileBasedConfigurationBuilder<INIConfiguration> builder =
-                configs.iniBuilder(filePath(TEST_INI));
+                configs.iniBuilder(absolutePath(TEST_INI));
         checkINI(builder.getConfiguration());
     }
 
@@ -436,7 +517,7 @@ public class TestConfigurations
     public void testINIFromPath() throws ConfigurationException
     {
         final Configurations configs = new Configurations();
-        final INIConfiguration config = configs.ini(filePath(TEST_INI));
+        final INIConfiguration config = configs.ini(absolutePath(TEST_INI));
         checkINI(config);
     }
 
@@ -512,7 +593,7 @@ public class TestConfigurations
     {
         final Configurations configs = new Configurations();
         final CombinedConfigurationBuilder builder =
-                configs.combinedBuilder(filePath(TEST_COMBINED));
+                configs.combinedBuilder(absolutePath(TEST_COMBINED));
         checkCombined(builder.getConfiguration());
     }
 
@@ -524,7 +605,7 @@ public class TestConfigurations
     {
         final Configurations configs = new Configurations();
         final CombinedConfiguration config =
-                configs.combined(filePath(TEST_COMBINED));
+                configs.combined(absolutePath(TEST_COMBINED));
         checkCombined(config);
     }
 }
diff --git a/src/test/java/org/apache/commons/configuration2/builder/fluent/TestParameters.java b/src/test/java/org/apache/commons/configuration2/builder/fluent/TestParameters.java
index 50f52b7..03d0785 100644
--- a/src/test/java/org/apache/commons/configuration2/builder/fluent/TestParameters.java
+++ b/src/test/java/org/apache/commons/configuration2/builder/fluent/TestParameters.java
@@ -25,6 +25,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.configuration2.ConfigurationConsumer;
 import org.apache.commons.configuration2.PropertiesConfiguration;
 import org.apache.commons.configuration2.builder.BasicBuilderParameters;
 import org.apache.commons.configuration2.builder.BasicBuilderProperties;
@@ -35,6 +36,7 @@ import org.apache.commons.configuration2.builder.FileBasedBuilderParametersImpl;
 import org.apache.commons.configuration2.builder.combined.CombinedBuilderParametersImpl;
 import org.apache.commons.configuration2.builder.combined.MultiFileBuilderParametersImpl;
 import org.apache.commons.configuration2.convert.ListDelimiterHandler;
+import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.tree.ExpressionEngine;
 import org.easymock.EasyMock;
 import org.easymock.IAnswer;
@@ -272,11 +274,19 @@ public class TestParameters
     {
         final PropertiesConfiguration.IOFactory factory =
                 EasyMock.createMock(PropertiesConfiguration.IOFactory.class);
+        final ConfigurationConsumer<ConfigurationException> includeListener =
+                EasyMock.createMock(ConfigurationConsumer.class);
+        // @formatter:off
         final Map<String, Object> map =
-                new Parameters().properties().setThrowExceptionOnMissing(true)
-                        .setFileName("test.properties").setIOFactory(factory)
-                        .setListDelimiterHandler(listHandler).setIncludesAllowed(false)
+                new Parameters().properties()
+                        .setThrowExceptionOnMissing(true)
+                        .setFileName("test.properties")
+                        .setIncludeListener(includeListener)
+                        .setIOFactory(factory)
+                        .setListDelimiterHandler(listHandler)
+                        .setIncludesAllowed(false)
                         .getParameters();
+        // @formatter:on
         checkBasicProperties(map);
         final FileBasedBuilderParametersImpl fbp =
                 FileBasedBuilderParametersImpl.fromParameters(map);
@@ -284,6 +294,7 @@ public class TestParameters
                 .getFileName());
         assertEquals("Wrong includes flag", Boolean.FALSE,
                 map.get("includesAllowed"));
+        assertSame("Wrong include listener", includeListener, map.get("includeListener"));
         assertSame("Wrong factory", factory, map.get("IOFactory"));
     }
 
diff --git a/src/test/resources/include-cyclical-reference.properties b/src/test/resources/include-cyclical-reference.properties
new file mode 100644
index 0000000..7a5ccf4
--- /dev/null
+++ b/src/test/resources/include-cyclical-reference.properties
@@ -0,0 +1,16 @@
+#   Licensed to the Apache Software Foundation (ASF) under one or more
+#   contributor license agreements.  See the NOTICE file distributed with
+#   this work for additional information regarding copyright ownership.
+#   The ASF licenses this file to You under the Apache License, Version 2.0
+#   (the "License"); you may not use this file except in compliance with
+#   the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+include = src/test/resources/include-cyclical-reference.properties
+keyA = valueA
diff --git a/src/test/resources/include-include-not-found.properties b/src/test/resources/include-include-not-found.properties
new file mode 100644
index 0000000..d0c400c
--- /dev/null
+++ b/src/test/resources/include-include-not-found.properties
@@ -0,0 +1,16 @@
+#   Licensed to the Apache Software Foundation (ASF) under one or more
+#   contributor license agreements.  See the NOTICE file distributed with
+#   this work for additional information regarding copyright ownership.
+#   The ASF licenses this file to You under the Apache License, Version 2.0
+#   (the "License"); you may not use this file except in compliance with
+#   the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+include = include-not-found.properties
+keyB = valueB
diff --git a/src/test/resources/include-load-exception.properties b/src/test/resources/include-load-exception.properties
new file mode 100644
index 0000000..87334cb
--- /dev/null
+++ b/src/test/resources/include-load-exception.properties
@@ -0,0 +1,16 @@
+#   Licensed to the Apache Software Foundation (ASF) under one or more
+#   contributor license agreements.  See the NOTICE file distributed with
+#   this work for additional information regarding copyright ownership.
+#   The ASF licenses this file to You under the Apache License, Version 2.0
+#   (the "License"); you may not use this file except in compliance with
+#   the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+include = http://this-file-does-not-and-should-not-exist.txt
+keyA = valueA
diff --git a/src/test/resources/include-not-found.properties b/src/test/resources/include-not-found.properties
new file mode 100644
index 0000000..056c086
--- /dev/null
+++ b/src/test/resources/include-not-found.properties
@@ -0,0 +1,16 @@
+#   Licensed to the Apache Software Foundation (ASF) under one or more
+#   contributor license agreements.  See the NOTICE file distributed with
+#   this work for additional information regarding copyright ownership.
+#   The ASF licenses this file to You under the Apache License, Version 2.0
+#   (the "License"); you may not use this file except in compliance with
+#   the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+include = this-file-does-not-and-should-not-exist.txt
+keyA = valueA