Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fallback ConfigReferenceResolver #497

Merged
merged 1 commit into from
Oct 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}"))
}

}