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") } + } }