mirror of
https://github.com/lightbend/config.git
synced 2025-03-22 23:30:27 +08:00
Add fallback ConfigReferenceResolver
This commit is contained in:
parent
7943a8ca98
commit
0a5603fe38
@ -29,10 +29,13 @@ package com.typesafe.config;
|
|||||||
public final class ConfigResolveOptions {
|
public final class ConfigResolveOptions {
|
||||||
private final boolean useSystemEnvironment;
|
private final boolean useSystemEnvironment;
|
||||||
private final boolean allowUnresolved;
|
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.useSystemEnvironment = useSystemEnvironment;
|
||||||
this.allowUnresolved = allowUnresolved;
|
this.allowUnresolved = allowUnresolved;
|
||||||
|
this.resolver = resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,7 +45,7 @@ public final class ConfigResolveOptions {
|
|||||||
* @return the default resolve options
|
* @return the default resolve options
|
||||||
*/
|
*/
|
||||||
public static ConfigResolveOptions defaults() {
|
public static ConfigResolveOptions defaults() {
|
||||||
return new ConfigResolveOptions(true, false);
|
return new ConfigResolveOptions(true, false, NULL_RESOLVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,7 +67,7 @@ public final class ConfigResolveOptions {
|
|||||||
* @return options with requested setting for use of environment variables
|
* @return options with requested setting for use of environment variables
|
||||||
*/
|
*/
|
||||||
public ConfigResolveOptions setUseSystemEnvironment(boolean value) {
|
public ConfigResolveOptions setUseSystemEnvironment(boolean value) {
|
||||||
return new ConfigResolveOptions(value, allowUnresolved);
|
return new ConfigResolveOptions(value, allowUnresolved, resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,7 +94,55 @@ public final class ConfigResolveOptions {
|
|||||||
* @since 1.2.0
|
* @since 1.2.0
|
||||||
*/
|
*/
|
||||||
public ConfigResolveOptions setAllowUnresolved(boolean value) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,4 +155,22 @@ public final class ConfigResolveOptions {
|
|||||||
public boolean getAllowUnresolved() {
|
public boolean getAllowUnresolved() {
|
||||||
return allowUnresolved;
|
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
config/src/main/java/com/typesafe/config/ConfigResolver.java
Normal file
38
config/src/main/java/com/typesafe/config/ConfigResolver.java
Normal file
@ -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);
|
||||||
|
|
||||||
|
}
|
@ -6,6 +6,8 @@ import java.util.Collections;
|
|||||||
import com.typesafe.config.ConfigException;
|
import com.typesafe.config.ConfigException;
|
||||||
import com.typesafe.config.ConfigOrigin;
|
import com.typesafe.config.ConfigOrigin;
|
||||||
import com.typesafe.config.ConfigRenderOptions;
|
import com.typesafe.config.ConfigRenderOptions;
|
||||||
|
import com.typesafe.config.ConfigResolveOptions;
|
||||||
|
import com.typesafe.config.ConfigValue;
|
||||||
import com.typesafe.config.ConfigValueType;
|
import com.typesafe.config.ConfigValueType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,7 +90,8 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
|
|||||||
v = result.value;
|
v = result.value;
|
||||||
newContext = result.context;
|
newContext = result.context;
|
||||||
} else {
|
} else {
|
||||||
v = null;
|
ConfigValue fallback = context.options().getResolver().lookup(expr.path().render());
|
||||||
|
v = (AbstractConfigValue) fallback;
|
||||||
}
|
}
|
||||||
} catch (NotPossibleToResolve e) {
|
} catch (NotPossibleToResolve e) {
|
||||||
if (ConfigImpl.traceSubstitutionsEnabled())
|
if (ConfigImpl.traceSubstitutionsEnabled())
|
||||||
|
@ -1233,4 +1233,70 @@ class ConfigTest extends TestUtils {
|
|||||||
val resolved = unresolved.resolveWith(source)
|
val resolved = unresolved.resolveWith(source)
|
||||||
assertEquals(43, resolved.getInt("foo"))
|
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}"))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user