From 92dc50ea0a52f3d6174a13b3af235c9722356544 Mon Sep 17 00:00:00 2001
From: Havoc Pennington <hp@pobox.com>
Date: Tue, 24 Mar 2015 11:33:29 -0400
Subject: [PATCH] Add Config.hasPathOrNull and Config.getIsNull

This is for #186 / #282 as an alternative to adding
a ton of getFooOrNull methods. With these methods apps
can handle null or missing settings in a special way
if they see fit.
---
 .../main/java/com/typesafe/config/Config.java | 68 +++++++++++++++++++
 .../typesafe/config/impl/SimpleConfig.java    | 59 +++++++++++++---
 .../typesafe/config/impl/PublicApiTest.scala  | 34 ++++++++++
 3 files changed, 152 insertions(+), 9 deletions(-)

diff --git a/config/src/main/java/com/typesafe/config/Config.java b/config/src/main/java/com/typesafe/config/Config.java
index 4e68148a..18de9d80 100644
--- a/config/src/main/java/com/typesafe/config/Config.java
+++ b/config/src/main/java/com/typesafe/config/Config.java
@@ -416,6 +416,47 @@ public interface Config extends ConfigMergeable {
      */
     boolean hasPath(String path);
 
+    /**
+     * Checks whether a value is present at the given path, even
+     * if the value is null. Most of the getters on
+     * <code>Config</code> will throw if you try to get a null
+     * value, so if you plan to call {@link #getValue(String)},
+     * {@link #getInt(String)}, or another getter you may want to
+     * use plain {@link #hasPath(String)} rather than this method.
+     *
+     * <p>
+     * To handle all three cases (unset, null, and a non-null value)
+     * the code might look like:
+     * <code><pre>
+     * if (config.hasPathOrNull(path)) {
+     *     if (config.getIsNull(path)) {
+     *        // handle null setting
+     *     } else {
+     *        // get and use non-null setting
+     *     }
+     * } else {
+     *     // handle entirely unset path
+     * }
+     * </pre></code>
+     *
+     * <p> However, the usual thing is to allow entirely unset
+     * paths to be a bug that throws an exception (because you set
+     * a default in your <code>reference.conf</code>), so in that
+     * case it's OK to call {@link #getIsNull(String)} without
+     * checking <code>hasPathOrNull</code> first.
+     *
+     * <p>
+     * Note that path expressions have a syntax and sometimes require quoting
+     * (see {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath}).
+     *
+     * @param path
+     *            the path expression
+     * @return true if a value is present at the path, even if the value is null
+     * @throws ConfigException.BadPath
+     *             if the path expression is invalid
+     */
+    boolean hasPathOrNull(String path);
+
     /**
      * Returns true if the {@code Config}'s root object contains no key-value
      * pairs.
@@ -448,6 +489,33 @@ public interface Config extends ConfigMergeable {
      */
     Set<Map.Entry<String, ConfigValue>> entrySet();
 
+    /**
+     * Checks whether a value is set to null at the given path,
+     * but throws an exception if the value is entirely
+     * unset. This method will not throw if {@link
+     * #hasPathOrNull(String)} returned true for the same path, so
+     * to avoid any possible exception check
+     * <code>hasPathOrNull()</code> first.  However, an exception
+     * for unset paths will usually be the right thing (because a
+     * <code>reference.conf</code> should exist that has the path
+     * set, the path should never be unset unless something is
+     * broken).
+     *
+     * <p>
+     * Note that path expressions have a syntax and sometimes require quoting
+     * (see {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath}).
+     *
+     * @param path
+     *            the path expression
+     * @return true if the value exists and is null, false if it
+     * exists and is not null
+     * @throws ConfigException.BadPath
+     *             if the path expression is invalid
+     * @throws ConfigException.Missing
+     *             if value is not set at all
+     */
+    boolean getIsNull(String path);
+
     /**
      *
      * @param path
diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java
index 6c6b93f8..26984354 100644
--- a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java
+++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java
@@ -79,8 +79,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
             return new SimpleConfig((AbstractConfigObject) resolved);
     }
 
-    @Override
-    public boolean hasPath(String pathExpression) {
+    private ConfigValue hasPathPeek(String pathExpression) {
         Path path = Path.newPath(pathExpression);
         ConfigValue peeked;
         try {
@@ -88,9 +87,21 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
         } catch (ConfigException.NotResolved e) {
             throw ConfigImpl.improveNotResolved(path, e);
         }
+        return peeked;
+    }
+
+    @Override
+    public boolean hasPath(String pathExpression) {
+        ConfigValue peeked = hasPathPeek(pathExpression);
         return peeked != null && peeked.valueType() != ConfigValueType.NULL;
     }
 
+    @Override
+    public boolean hasPathOrNull(String path) {
+        ConfigValue peeked = hasPathPeek(path);
+        return peeked != null;
+    }
+
     @Override
     public boolean isEmpty() {
         return object.isEmpty();
@@ -121,8 +132,21 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
         return entries;
     }
 
+    static private AbstractConfigValue throwIfNull(AbstractConfigValue v, ConfigValueType expected, Path originalPath) {
+        if (v.valueType() == ConfigValueType.NULL)
+            throw new ConfigException.Null(v.origin(), originalPath.render(),
+                    expected != null ? expected.name() : null);
+        else
+            return v;
+    }
+
     static private AbstractConfigValue findKey(AbstractConfigObject self, String key,
             ConfigValueType expected, Path originalPath) {
+        return throwIfNull(findKeyOrNull(self, key, expected, originalPath), expected, originalPath);
+    }
+
+    static private AbstractConfigValue findKeyOrNull(AbstractConfigObject self, String key,
+            ConfigValueType expected, Path originalPath) {
         AbstractConfigValue v = self.peekAssumingResolved(key, originalPath);
         if (v == null)
             throw new ConfigException.Missing(originalPath.render());
@@ -130,10 +154,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
         if (expected != null)
             v = DefaultTransformer.transform(v, expected);
 
-        if (v.valueType() == ConfigValueType.NULL)
-            throw new ConfigException.Null(v.origin(), originalPath.render(),
-                    expected != null ? expected.name() : null);
-        else if (expected != null && v.valueType() != expected)
+        if (expected != null && (v.valueType() != expected && v.valueType() != ConfigValueType.NULL))
             throw new ConfigException.WrongType(v.origin(), originalPath.render(), expected.name(),
                     v.valueType().name());
         else
@@ -142,17 +163,22 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
 
     static private AbstractConfigValue find(AbstractConfigObject self, Path path,
             ConfigValueType expected, Path originalPath) {
+        return throwIfNull(findOrNull(self, path, expected, originalPath), expected, originalPath);
+    }
+
+    static private AbstractConfigValue findOrNull(AbstractConfigObject self, Path path,
+            ConfigValueType expected, Path originalPath) {
         try {
             String key = path.first();
             Path next = path.remainder();
             if (next == null) {
-                return findKey(self, key, expected, originalPath);
+                return findKeyOrNull(self, key, expected, originalPath);
             } else {
                 AbstractConfigObject o = (AbstractConfigObject) findKey(self, key,
                         ConfigValueType.OBJECT,
                         originalPath.subPath(0, originalPath.length() - next.length()));
                 assert (o != null); // missing was supposed to throw
-                return find(o, next, expected, originalPath);
+                return findOrNull(o, next, expected, originalPath);
             }
         } catch (ConfigException.NotResolved e) {
             throw ConfigImpl.improveNotResolved(path, e);
@@ -160,7 +186,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
     }
 
     AbstractConfigValue find(Path pathExpression, ConfigValueType expected, Path originalPath) {
-        return find(object, pathExpression, expected, originalPath);
+        return throwIfNull(findOrNull(object, pathExpression, expected, originalPath), expected, originalPath);
     }
 
     AbstractConfigValue find(String pathExpression, ConfigValueType expected) {
@@ -168,11 +194,26 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
         return find(path, expected, path);
     }
 
+    private AbstractConfigValue findOrNull(Path pathExpression, ConfigValueType expected, Path originalPath) {
+        return findOrNull(object, pathExpression, expected, originalPath);
+    }
+
+    private AbstractConfigValue findOrNull(String pathExpression, ConfigValueType expected) {
+        Path path = Path.newPath(pathExpression);
+        return findOrNull(path, expected, path);
+    }
+
     @Override
     public AbstractConfigValue getValue(String path) {
         return find(path, null);
     }
 
+    @Override
+    public boolean getIsNull(String path) {
+        AbstractConfigValue v = findOrNull(path, null);
+        return (v.valueType() == ConfigValueType.NULL);
+    }
+
     @Override
     public boolean getBoolean(String path) {
         ConfigValue v = find(path, ConfigValueType.BOOLEAN);
diff --git a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala
index 0150d271..67ea0155 100644
--- a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala
+++ b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala
@@ -1057,4 +1057,38 @@ include "onclasspath"
         assertFalse("did not get bar-file.conf", conf.hasPath("bar-file"))
         assertFalse("did not get subdir/baz.conf", conf.hasPath("baz"))
     }
+
+    @Test
+    def hasPathOrNullWorks(): Unit = {
+        val conf = ConfigFactory.parseString("x.a=null,x.b=42")
+        assertFalse("hasPath says false for null", conf.hasPath("x.a"))
+        assertTrue("hasPathOrNull says true for null", conf.hasPathOrNull("x.a"))
+
+        assertTrue("hasPath says true for non-null", conf.hasPath("x.b"))
+        assertTrue("hasPathOrNull says true for non-null", conf.hasPathOrNull("x.b"))
+
+        assertFalse("hasPath says false for missing", conf.hasPath("x.c"))
+        assertFalse("hasPathOrNull says false for missing", conf.hasPathOrNull("x.c"))
+
+        // this is to be sure we handle a null along the path correctly
+        assertFalse("hasPath says false for missing under null", conf.hasPath("x.a.y"))
+        assertFalse("hasPathOrNull says false for missing under null", conf.hasPathOrNull("x.a.y"))
+
+        // this is to be sure we handle missing along the path correctly
+        assertFalse("hasPath says false for missing under missing", conf.hasPath("x.c.y"))
+        assertFalse("hasPathOrNull says false for missing under missing", conf.hasPathOrNull("x.c.y"))
+    }
+
+    @Test
+    def getIsNullWorks(): Unit = {
+        val conf = ConfigFactory.parseString("x.a=null,x.b=42")
+
+        assertTrue("getIsNull says true for null", conf.getIsNull("x.a"))
+        assertFalse("getIsNull says false for non-null", conf.getIsNull("x.b"))
+        intercept[ConfigException.Missing] { conf.getIsNull("x.c") }
+        // missing underneath null
+        intercept[ConfigException.Missing] { conf.getIsNull("x.a.y") }
+        // missing underneath missing
+        intercept[ConfigException.Missing] { conf.getIsNull("x.c.y") }
+    }
 }