From 8c0afc58d99c7ac01bfb631e9b1d9bd58bcb6d4e Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Fri, 3 Jan 2014 09:57:33 -0500 Subject: [PATCH 1/3] Add ConfigResolveOptions.allowUnresolved This allows Config.resolve() to do a partial resolve. Fixes #100 --- .../typesafe/config/ConfigResolveOptions.java | 42 ++++++++++++++++--- .../typesafe/config/impl/ConfigReference.java | 8 +++- .../typesafe/config/impl/ResolveContext.java | 6 ++- .../com/typesafe/config/impl/ConfigTest.scala | 26 ++++++++++++ 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java b/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java index d82a6be7..7a3f49bb 100644 --- a/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java +++ b/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java @@ -9,6 +9,9 @@ package com.typesafe.config; * href="https://github.com/typesafehub/config/blob/master/HOCON.md">HOCON * spec. *

+ * Typically this class would be used with the method + * {@link Config#resolve(ConfigResolveOptions)}. + *

* This object is immutable, so the "setters" return a new object. *

* Here is an example of creating a custom {@code ConfigResolveOptions}: @@ -25,18 +28,21 @@ package com.typesafe.config; */ public final class ConfigResolveOptions { private final boolean useSystemEnvironment; + private final boolean allowUnresolved; - private ConfigResolveOptions(boolean useSystemEnvironment) { + private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolved) { this.useSystemEnvironment = useSystemEnvironment; + this.allowUnresolved = allowUnresolved; } /** - * Returns the default resolve options. - * + * Returns the default resolve options. By default the system environment + * will be used and unresolved substitutions are not allowed. + * * @return the default resolve options */ public static ConfigResolveOptions defaults() { - return new ConfigResolveOptions(true); + return new ConfigResolveOptions(true, false); } /** @@ -57,9 +63,8 @@ public final class ConfigResolveOptions { * variables. * @return options with requested setting for use of environment variables */ - @SuppressWarnings("static-method") public ConfigResolveOptions setUseSystemEnvironment(boolean value) { - return new ConfigResolveOptions(value); + return new ConfigResolveOptions(value, allowUnresolved); } /** @@ -72,4 +77,29 @@ public final class ConfigResolveOptions { public boolean getUseSystemEnvironment() { return useSystemEnvironment; } + + /** + * Returns options with "allow unresolved" set to the given value. By + * default, unresolved substitutions are an error. If unresolved + * substitutions are allowed, then a future attempt to use the unresolved + * value may fail, but {@link Config#resolve(ConfigResolveOptions)} itself + * will now throw. + * + * @param value + * true to silently ignore unresolved substitutions. + * @return options with requested setting for whether to allow substitutions + */ + public ConfigResolveOptions setAllowUnresolved(boolean value) { + return new ConfigResolveOptions(useSystemEnvironment, value); + } + + /** + * Returns whether the options allow unresolved substitutions. This method + * is mostly used by the config lib internally, not by applications. + * + * @return true if unresolved substitutions are allowed + */ + public boolean getAllowUnresolved() { + return allowUnresolved; + } } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java index 880c0ef1..400fb713 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java @@ -81,9 +81,13 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable { } if (v == null && !expr.optional()) { - throw new ConfigException.UnresolvedSubstitution(origin(), expr.toString()); + if (context.options().getAllowUnresolved()) + return this; + else + throw new ConfigException.UnresolvedSubstitution(origin(), expr.toString()); + } else { + return v; } - return v; } finally { context.source().unreplace(this); } diff --git a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java index d7fe99bd..b73c064c 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java @@ -121,14 +121,16 @@ final class ResolveContext { memos.put(fullKey, resolved); } else { // if we have an unresolved object then either we did a - // partial resolve restricted to a certain child, or it's - // a bug. + // partial resolve restricted to a certain child, or we are + // allowing incomplete resolution, or it's a bug. if (isRestrictedToChild()) { if (restrictedKey == null) { throw new ConfigException.BugOrBroken( "restrictedKey should not be null here"); } memos.put(restrictedKey, resolved); + } else if (options().getAllowUnresolved()) { + memos.put(fullKey, resolved); } else { throw new ConfigException.BugOrBroken( "resolveSubstitutions() did not give us a resolved object"); diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index 87b281b9..157680a1 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -1103,4 +1103,30 @@ class ConfigTest extends TestUtils { checkSerializable(resolved) } } + + @Test + def allowUnresolvedDoesAllowUnresolved() { + val values = ConfigFactory.parseString("{ foo = 1, bar = 2, m = 3, n = 4}") + val unresolved = ConfigFactory.parseString("a = ${foo}, b = ${bar}, c { x = ${m}, y = ${n} }, alwaysResolveable=${alwaysValue}, alwaysValue=42") + // resolve() by default throws with unresolveable substs + intercept[ConfigException.UnresolvedSubstitution] { + unresolved.resolve(ConfigResolveOptions.defaults()) + } + // we shouldn't be able to get a value without resolving it + intercept[ConfigException.NotResolved] { + unresolved.getInt("alwaysResolveable") + } + val allowedUnresolved = unresolved.resolve(ConfigResolveOptions.defaults().setAllowUnresolved(true)) + // when we partially-resolve we should still resolve what we can + assertEquals("we resolved the resolveable", 42, allowedUnresolved.getInt("alwaysResolveable")) + // but unresolved should still all throw + for (k <- Seq("a", "b", "c.x", "c.y")) { + intercept[ConfigException.NotResolved] { allowedUnresolved.getInt(k) } + } + // and given the values for the resolve, we should be able to + val resolved = allowedUnresolved.withFallback(values).resolve() + for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) { + assertEquals(kv._2, resolved.getInt(kv._1)) + } + } } From d7287c9e16786f35111c1f8c7cfddcebe32585ae Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Fri, 3 Jan 2014 10:27:44 -0500 Subject: [PATCH 2/3] add a Config.isResolved method Now that partial resolution is allowed, this may be useful for people to check after they do a partial resolve. --- .../main/java/com/typesafe/config/Config.java | 14 +++++++++++++- .../com/typesafe/config/impl/SimpleConfig.java | 5 +++++ .../com/typesafe/config/impl/ConfigTest.scala | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/com/typesafe/config/Config.java b/config/src/main/java/com/typesafe/config/Config.java index 42e4df0c..942697b9 100644 --- a/config/src/main/java/com/typesafe/config/Config.java +++ b/config/src/main/java/com/typesafe/config/Config.java @@ -168,10 +168,22 @@ public interface Config extends ConfigMergeable { * * @param options * resolve options - * @return the resolved Config + * @return the resolved Config (may be only partially resolved if options are set to allow unresolved) */ Config resolve(ConfigResolveOptions options); + /** + * Checks whether the config is completely resolved. After a successful call to + * {@link Config#resolve()} it will be completely resolved, but after calling + * {@link Config#resolve(ConfigResolveOptions)} with allowUnresolved set + * in the options, it may or may not be completely resolved. A newly-loaded config + * may or may not be completely resolved depending on whether there were substitutions + * present in the file. + * + * @return true if there are no unresolved substitutions remaining in this configuration. + */ + boolean isResolved(); + /** * Validates this config against a reference config, throwing an exception * if it is invalid. The purpose of this method is to "fail early" with a 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 d3be5cf4..e7c8f695 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -815,6 +815,11 @@ final class SimpleConfig implements Config, MergeableValue, Serializable { } } + @Override + public boolean isResolved() { + return root().resolveStatus() == ResolveStatus.RESOLVED; + } + @Override public void checkValid(Config reference, String... restrictToPaths) { SimpleConfig ref = (SimpleConfig) reference; diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index 157680a1..26b85388 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -1104,10 +1104,23 @@ class ConfigTest extends TestUtils { } } + @Test + def isResolvedWorks() { + val resolved = ConfigFactory.parseString("foo = 1") + assertTrue("config with no substitutions starts as resolved", resolved.isResolved) + val unresolved = ConfigFactory.parseString("foo = ${a}, a=42") + assertFalse("config with substitutions starts as not resolved", unresolved.isResolved) + val resolved2 = unresolved.resolve() + assertTrue("after resolution, config is now resolved", resolved2.isResolved) + } + @Test def allowUnresolvedDoesAllowUnresolved() { val values = ConfigFactory.parseString("{ foo = 1, bar = 2, m = 3, n = 4}") + assertTrue("config with no substitutions starts as resolved", values.isResolved) val unresolved = ConfigFactory.parseString("a = ${foo}, b = ${bar}, c { x = ${m}, y = ${n} }, alwaysResolveable=${alwaysValue}, alwaysValue=42") + assertFalse("config with substitutions starts as not resolved", unresolved.isResolved) + // resolve() by default throws with unresolveable substs intercept[ConfigException.UnresolvedSubstitution] { unresolved.resolve(ConfigResolveOptions.defaults()) @@ -1123,10 +1136,13 @@ class ConfigTest extends TestUtils { for (k <- Seq("a", "b", "c.x", "c.y")) { intercept[ConfigException.NotResolved] { allowedUnresolved.getInt(k) } } + // and the partially-resolved thing is not resolved + assertFalse("partially-resolved object is not resolved", allowedUnresolved.isResolved) // and given the values for the resolve, we should be able to val resolved = allowedUnresolved.withFallback(values).resolve() for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) { assertEquals(kv._2, resolved.getInt(kv._1)) } + assertTrue("fully resolved object is resolved", resolved.isResolved) } } From 247236c6f1fd7d7d0348c64a5460c553ac84e338 Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Fri, 3 Jan 2014 11:02:45 -0500 Subject: [PATCH 3/3] add Config.resolveWith() This allows replacing substitutions with values which are not merged into the Config. Fixes #95 --- .../main/java/com/typesafe/config/Config.java | 40 +++++++++++++++++++ .../typesafe/config/impl/SimpleConfig.java | 13 +++++- .../com/typesafe/config/impl/ConfigTest.scala | 33 ++++++++++++--- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/config/src/main/java/com/typesafe/config/Config.java b/config/src/main/java/com/typesafe/config/Config.java index 942697b9..7ed754cc 100644 --- a/config/src/main/java/com/typesafe/config/Config.java +++ b/config/src/main/java/com/typesafe/config/Config.java @@ -184,6 +184,46 @@ public interface Config extends ConfigMergeable { */ boolean isResolved(); + /** + * Like {@link Config#resolve()} except that substitution values are looked + * up in the given source, rather than in this instance. This is a + * special-purpose method which doesn't make sense to use in most cases; + * it's only needed if you're constructing some sort of app-specific custom + * approach to configuration. The more usual approach if you have a source + * of substitution values would be to merge that source into your config + * stack using {@link Config#withFallback} and then resolve. + *

+ * Note that this method does NOT look in this instance for substitution + * values. If you want to do that, you could either merge this instance into + * your value source using {@link Config#withFallback}, or you could resolve + * multiple times with multiple sources (using + * {@link ConfigResolveOptions#setAllowUnresolved(boolean)} so the partial + * resolves don't fail). + * + * @param source + * configuration to pull values from + * @return an immutable object with substitutions resolved + * @throws ConfigException.UnresolvedSubstitution + * if any substitutions refer to paths which are not in the + * source + * @throws ConfigException + * some other config exception if there are other problems + */ + Config resolveWith(Config source); + + /** + * Like {@link Config#resolveWith(Config)} but allows you to specify + * non-default options. + * + * @param source + * source configuration to pull values from + * @param options + * resolve options + * @return the resolved Config (may be only partially resolved + * if options are set to allow unresolved) + */ + Config resolveWith(Config source, ConfigResolveOptions options); + /** * Validates this config against a reference config, throwing an exception * if it is invalid. The purpose of this method is to "fail early" with a 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 e7c8f695..f54dcd18 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -57,7 +57,17 @@ final class SimpleConfig implements Config, MergeableValue, Serializable { @Override public SimpleConfig resolve(ConfigResolveOptions options) { - AbstractConfigValue resolved = ResolveContext.resolve(object, object, options); + return resolveWith(this, options); + } + + @Override + public SimpleConfig resolveWith(Config source) { + return resolveWith(source, ConfigResolveOptions.defaults()); + } + + @Override + public SimpleConfig resolveWith(Config source, ConfigResolveOptions options) { + AbstractConfigValue resolved = ResolveContext.resolve(object, ((SimpleConfig) source).object, options); if (resolved == object) return this; @@ -65,7 +75,6 @@ final class SimpleConfig implements Config, MergeableValue, Serializable { return new SimpleConfig((AbstractConfigObject) resolved); } - @Override public boolean hasPath(String pathExpression) { Path path = Path.newPath(pathExpression); diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index 26b85388..8c58a510 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -1138,11 +1138,34 @@ class ConfigTest extends TestUtils { } // and the partially-resolved thing is not resolved assertFalse("partially-resolved object is not resolved", allowedUnresolved.isResolved) - // and given the values for the resolve, we should be able to - val resolved = allowedUnresolved.withFallback(values).resolve() - for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) { - assertEquals(kv._2, resolved.getInt(kv._1)) + + // scope "val resolved" + { + // and given the values for the resolve, we should be able to + val resolved = allowedUnresolved.withFallback(values).resolve() + for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) { + assertEquals(kv._2, resolved.getInt(kv._1)) + } + assertTrue("fully resolved object is resolved", resolved.isResolved) } - assertTrue("fully resolved object is resolved", resolved.isResolved) + + // we should also be able to use resolveWith + { + val resolved = allowedUnresolved.resolveWith(values) + for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) { + assertEquals(kv._2, resolved.getInt(kv._1)) + } + assertTrue("fully resolved object is resolved", resolved.isResolved) + } + } + + @Test + def resolveWithWorks(): Unit = { + // the a=42 is present here to be sure it gets ignored when we resolveWith + val unresolved = ConfigFactory.parseString("foo = ${a}, a = 42") + assertEquals(42, unresolved.resolve().getInt("foo")) + val source = ConfigFactory.parseString("a = 43") + val resolved = unresolved.resolveWith(source) + assertEquals(43, resolved.getInt("foo")) } }