Skip to content

Commit

Permalink
Merge pull request lightbend#497 from plesner/resolver
Browse files Browse the repository at this point in the history
Add fallback ConfigReferenceResolver
  • Loading branch information
havocp authored Oct 2, 2017
2 parents 7943a8c + 0a5603f commit 527c7f5
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 5 deletions.
77 changes: 73 additions & 4 deletions config/src/main/java/com/typesafe/config/ConfigResolveOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
public final class ConfigResolveOptions {
private final boolean useSystemEnvironment;
private final boolean allowUnresolved;
private final ConfigResolver resolver;

private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolved) {
private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolved,
ConfigResolver resolver) {
this.useSystemEnvironment = useSystemEnvironment;
this.allowUnresolved = allowUnresolved;
this.resolver = resolver;
}

/**
Expand All @@ -42,7 +45,7 @@ private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolv
* @return the default resolve options
*/
public static ConfigResolveOptions defaults() {
return new ConfigResolveOptions(true, false);
return new ConfigResolveOptions(true, false, NULL_RESOLVER);
}

/**
Expand All @@ -64,7 +67,7 @@ public static ConfigResolveOptions noSystem() {
* @return options with requested setting for use of environment variables
*/
public ConfigResolveOptions setUseSystemEnvironment(boolean value) {
return new ConfigResolveOptions(value, allowUnresolved);
return new ConfigResolveOptions(value, allowUnresolved, resolver);
}

/**
Expand All @@ -91,7 +94,55 @@ public boolean getUseSystemEnvironment() {
* @since 1.2.0
*/
public ConfigResolveOptions setAllowUnresolved(boolean value) {
return new ConfigResolveOptions(useSystemEnvironment, value);
return new ConfigResolveOptions(useSystemEnvironment, value, resolver);
}

/**
* Returns options where the given resolver used as a fallback if a
* reference cannot be otherwise resolved. This resolver will only be called
* after resolution has failed to substitute with a value from within the
* config itself and with any other resolvers that have been appended before
* this one. Multiple resolvers can be added using,
*
* <pre>
* ConfigResolveOptions options = ConfigResolveOptions.defaults()
* .appendResolver(primary)
* .appendResolver(secondary)
* .appendResolver(tertiary);
* </pre>
*
* With this config unresolved references will first be resolved with the
* primary resolver, if that fails then the secondary, and finally if that
* also fails the tertiary.
*
* If all fallbacks fail to return a substitution "allow unresolved"
* determines whether resolution fails or continues.
*`
* @param value the resolver to fall back to
* @return options that use the given resolver as a fallback
* @since 1.3.2
*/
public ConfigResolveOptions appendResolver(ConfigResolver value) {
if (value == null) {
throw new ConfigException.BugOrBroken("null resolver passed to appendResolver");
} else if (value == this.resolver) {
return this;
} else {
return new ConfigResolveOptions(useSystemEnvironment, allowUnresolved,
this.resolver.withFallback(value));
}
}

/**
* Returns the resolver to use as a fallback if a substitution cannot be
* otherwise resolved. Never returns null. This method is mostly used by the
* config lib internally, not by applications.
*
* @return the non-null fallback resolver
* @since 1.3.2
*/
public ConfigResolver getResolver() {
return this.resolver;
}

/**
Expand All @@ -104,4 +155,22 @@ public ConfigResolveOptions setAllowUnresolved(boolean value) {
public boolean getAllowUnresolved() {
return allowUnresolved;
}

/**
* Singleton resolver that never resolves paths.
*/
private static final ConfigResolver NULL_RESOLVER = new ConfigResolver() {

@Override
public ConfigValue lookup(String path) {
return null;
}

@Override
public ConfigResolver withFallback(ConfigResolver fallback) {
return fallback;
}

};

}
38 changes: 38 additions & 0 deletions config/src/main/java/com/typesafe/config/ConfigResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.typesafe.config;

/**
* Implement this interface and provide an instance to
* {@link ConfigResolveOptions#appendResolver ConfigResolveOptions.appendResolver()}
* to provide custom behavior when unresolved substitutions are encountered
* during resolution.
* @since 1.3.2
*/
public interface ConfigResolver {

/**
* Returns the value to substitute for the given unresolved path. To get the
* components of the path use {@link ConfigUtil#splitPath(String)}. If a
* non-null value is returned that value will be substituted, otherwise
* resolution will continue to consider the substitution as still
* unresolved.
*
* @param path the unresolved path
* @return the value to use as a substitution or null
*/
public ConfigValue lookup(String path);

/**
* Returns a new resolver that falls back to the given resolver if this
* one doesn't provide a substitution itself.
*
* It's important to handle the case where you already have the fallback
* with a "return this", i.e. this method should not create a new object if
* the fallback is the same one you already have. The same fallback may be
* added repeatedly.
*
* @param fallback the previous includer for chaining
* @return a new resolver
*/
public ConfigResolver withFallback(ConfigResolver fallback);

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;

/**
Expand Down Expand Up @@ -88,7 +90,8 @@ ResolveResult<? extends AbstractConfigValue> resolveSubstitutions(ResolveContext
v = result.value;
newContext = result.context;
} else {
v = null;
ConfigValue fallback = context.options().getResolver().lookup(expr.path().render());
v = (AbstractConfigValue) fallback;
}
} catch (NotPossibleToResolve e) {
if (ConfigImpl.traceSubstitutionsEnabled())
Expand Down
66 changes: 66 additions & 0 deletions config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1233,4 +1233,70 @@ class ConfigTest extends TestUtils {
val resolved = unresolved.resolveWith(source)
assertEquals(43, resolved.getInt("foo"))
}

/**
* A resolver that replaces paths that start with a particular prefix with
* strings where that prefix has been replaced with another prefix.
*/
class DummyResolver(prefix: String, newPrefix: String, fallback: ConfigResolver) extends ConfigResolver {

override def lookup(path: String): ConfigValue = {
if (path.startsWith(prefix))
ConfigValueFactory.fromAnyRef(newPrefix + path.substring(prefix.length))
else if (fallback != null)
fallback.lookup(path)
else
null
}

override def withFallback(f: ConfigResolver): ConfigResolver = {
if (fallback == null)
new DummyResolver(prefix, newPrefix, f)
else
new DummyResolver(prefix, newPrefix, fallback.withFallback(f))
}

}

private def runFallbackTest(expected: String, source: String,
allowUnresolved: Boolean, resolvers: ConfigResolver*) = {
val unresolved = ConfigFactory.parseString(source)
var options = ConfigResolveOptions.defaults().setAllowUnresolved(allowUnresolved)
for (resolver <- resolvers)
options = options.appendResolver(resolver)
val obj = unresolved.resolve(options).root()
assertEquals(expected, obj.render(ConfigRenderOptions.concise().setJson(false)))
}

@Test
def resolveFallback(): Unit = {
runFallbackTest(
"x=a,y=b",
"x=${a},y=${b}", false,
new DummyResolver("", "", null))
runFallbackTest(
"x=\"a.b.c\",y=\"a.b.d\"",
"x=${a.b.c},y=${a.b.d}", false,
new DummyResolver("", "", null))
runFallbackTest(
"x=${a.b.c},y=${a.b.d}",
"x=${a.b.c},y=${a.b.d}", true,
new DummyResolver("x.", "", null))
runFallbackTest(
"x=${a.b.c},y=\"e.f\"",
"x=${a.b.c},y=${d.e.f}", true,
new DummyResolver("d.", "", null))
runFallbackTest(
"w=\"Y.c.d\",x=${a},y=\"X.b\",z=\"Y.c\"",
"x=${a},y=${a.b},z=${a.b.c},w=${a.b.c.d}", true,
new DummyResolver("a.b.", "Y.", null),
new DummyResolver("a.", "X.", null))

runFallbackTest("x=${a.b.c}", "x=${a.b.c}", true, new DummyResolver("x.", "", null))
val e = intercept[ConfigException.UnresolvedSubstitution] {
runFallbackTest("x=${a.b.c}", "x=${a.b.c}", false, new DummyResolver("x.", "", null))
}
assertTrue(e.getMessage.contains("${a.b.c}"))
}

}

0 comments on commit 527c7f5

Please sign in to comment.