Redo substitution resolving to better handle complex cases.

The main idea of this patch is to introduce "partial resolution"
which means resolving only the minimum branch of the object tree
to get to a desired value. By using partial resolution whenever
possible, more interdependencies between substitutions are permitted.

ConfigDelayedMergeObject was a big problem because a lot of the
code in AbstractConfigObject really didn't work on it, because
it assumed a resolved object; much of that code now moves down
to SimpleConfigObject.
This commit is contained in:
Havoc Pennington 2012-02-24 23:22:25 -05:00
parent 021a958a93
commit a59e31f744
14 changed files with 822 additions and 336 deletions

View File

@ -6,12 +6,8 @@ package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigMergeable;
@ -57,66 +53,92 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
/**
* This looks up the key with no transformation or type conversion of any
* kind, and returns null if the key is not present.
* kind, and returns null if the key is not present. The object must be
* resolved; use attemptPeekWithPartialResolve() if it is not.
*
* @param key
* @return the unmodified raw value or null
*/
protected abstract AbstractConfigValue peek(String key);
protected AbstractConfigValue peek(String key,
SubstitutionResolver resolver, int depth,
ConfigResolveOptions options) {
AbstractConfigValue v = peek(key);
if (v != null && resolver != null) {
v = resolver.resolve(v, depth, options);
protected final AbstractConfigValue peekAssumingResolved(String key, String originalPath) {
try {
return attemptPeekWithPartialResolve(key);
} catch (NeedsFullResolve e) {
throw new ConfigException.NotResolved(originalPath + ": " + e.getMessage(), e);
}
return v;
}
/**
* Look up the key on an only-partially-resolved object, with no
* transformation or type conversion of any kind; if 'this' is not resolved
* then try to look up the key anyway if possible.
*
* @param key
* key to look up
* @return the value of the key, or null if known not to exist
* @throws NeedsFullResolve
* if can't figure out key's value or can't know whether it
* exists
*/
protected abstract AbstractConfigValue attemptPeekWithPartialResolve(String key)
throws NeedsFullResolve;
/**
* Looks up the path with no transformation, type conversion, or exceptions
* (just returns null if path not found). Does however resolve the path, if
* resolver != null.
*
* @throws NotPossibleToResolve
*/
protected AbstractConfigValue peekPath(Path path, SubstitutionResolver resolver,
int depth, ConfigResolveOptions options) {
protected AbstractConfigValue peekPath(Path path, SubstitutionResolver resolver, int depth,
ConfigResolveOptions options) throws NotPossibleToResolve, NeedsFullResolve {
return peekPath(this, path, resolver, depth, options);
}
AbstractConfigValue peekPath(Path path) {
return peekPath(this, path, null, 0, null);
/**
* Looks up the path and throws public API exceptions (ConfigException).
* Doesn't do any resolution, will throw if any is needed.
*/
AbstractConfigValue peekPathWithExternalExceptions(Path path) {
try {
return peekPath(this, path, null, 0, null);
} catch (NotPossibleToResolve e) {
throw e.exportException(origin(), path.render());
} catch (NeedsFullResolve e) {
throw new ConfigException.NotResolved(
"need to resolve() this Config before looking up value at " + path.render(), e);
}
}
// as a side effect, peekPath() will have to resolve all parents of the
// child being peeked, but NOT the child itself. Caller has to resolve
// the child itself if needed.
private static AbstractConfigValue peekPath(AbstractConfigObject self, Path path,
SubstitutionResolver resolver, int depth,
ConfigResolveOptions options) {
String key = path.first();
Path next = path.remainder();
if (next == null) {
AbstractConfigValue v = self.peek(key, resolver, depth, options);
return v;
} else {
// it's important to ONLY resolve substitutions here, not
// all values, because if you resolve arrays or objects
// it creates unnecessary cycles as a side effect (any sibling
// of the object we want to follow could cause a cycle, not just
// the object we want to follow).
ConfigValue v = self.peek(key);
if (v instanceof ConfigSubstitution && resolver != null) {
v = resolver.resolve((AbstractConfigValue) v, depth, options);
}
if (v instanceof AbstractConfigObject) {
return peekPath((AbstractConfigObject) v, next, resolver,
depth, options);
SubstitutionResolver resolver, int depth, ConfigResolveOptions options)
throws NotPossibleToResolve, NeedsFullResolve {
if (resolver != null) {
// walk down through the path resolving only things along that path,
// and then recursively call ourselves with no resolver.
AbstractConfigValue partiallyResolved = resolver.resolve(self, depth, options, path);
if (partiallyResolved instanceof AbstractConfigObject) {
return peekPath((AbstractConfigObject) partiallyResolved, path, null, 0, null);
} else {
return null;
throw new ConfigException.BugOrBroken("resolved object to non-object " + self
+ " to " + partiallyResolved);
}
} else {
// with no resolver, we'll fail if anything along the path can't be
// looked at without resolving.
Path next = path.remainder();
AbstractConfigValue v = self.attemptPeekWithPartialResolve(path.first());
if (next == null) {
return v;
} else {
if (v instanceof AbstractConfigObject) {
return peekPath((AbstractConfigObject) v, next, null, 0, null);
} else {
return null;
}
}
}
}
@ -151,47 +173,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
}
@Override
protected AbstractConfigObject mergedWithObject(AbstractConfigObject fallback) {
if (ignoresFallbacks())
throw new ConfigException.BugOrBroken("should not be reached");
boolean changed = false;
boolean allResolved = true;
Map<String, AbstractConfigValue> merged = new HashMap<String, AbstractConfigValue>();
Set<String> allKeys = new HashSet<String>();
allKeys.addAll(this.keySet());
allKeys.addAll(fallback.keySet());
for (String key : allKeys) {
AbstractConfigValue first = this.peek(key);
AbstractConfigValue second = fallback.peek(key);
AbstractConfigValue kept;
if (first == null)
kept = second;
else if (second == null)
kept = first;
else
kept = first.withFallback(second);
merged.put(key, kept);
if (first != kept)
changed = true;
if (kept.resolveStatus() == ResolveStatus.UNRESOLVED)
allResolved = false;
}
ResolveStatus newResolveStatus = ResolveStatus.fromBoolean(allResolved);
boolean newIgnoresFallbacks = fallback.ignoresFallbacks();
if (changed)
return new SimpleConfigObject(mergeOrigins(this, fallback), merged, newResolveStatus,
newIgnoresFallbacks);
else if (newResolveStatus != resolveStatus() || newIgnoresFallbacks != ignoresFallbacks())
return newCopy(newResolveStatus, newIgnoresFallbacks, origin());
else
return this;
}
protected abstract AbstractConfigObject mergedWithObject(AbstractConfigObject fallback);
@Override
public AbstractConfigObject withFallback(ConfigMergeable mergeable) {
@ -234,175 +216,23 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
return mergeOrigins(Arrays.asList(stack));
}
private AbstractConfigObject modify(Modifier modifier,
ResolveStatus newResolveStatus) {
Map<String, AbstractConfigValue> changes = null;
for (String k : keySet()) {
AbstractConfigValue v = peek(k);
// "modified" may be null, which means remove the child;
// to do that we put null in the "changes" map.
AbstractConfigValue modified = modifier.modifyChild(v);
if (modified != v) {
if (changes == null)
changes = new HashMap<String, AbstractConfigValue>();
changes.put(k, modified);
}
}
if (changes == null) {
return newCopy(newResolveStatus, ignoresFallbacks(), origin());
} else {
Map<String, AbstractConfigValue> modified = new HashMap<String, AbstractConfigValue>();
for (String k : keySet()) {
if (changes.containsKey(k)) {
AbstractConfigValue newValue = changes.get(k);
if (newValue != null) {
modified.put(k, newValue);
} else {
// remove this child; don't put it in the new map.
}
} else {
modified.put(k, peek(k));
}
}
return new SimpleConfigObject(origin(), modified, newResolveStatus,
ignoresFallbacks());
}
}
@Override
abstract AbstractConfigObject resolveSubstitutions(final SubstitutionResolver resolver,
int depth, ConfigResolveOptions options, Path restrictToChildOrNull)
throws NotPossibleToResolve, NeedsFullResolve;
@Override
AbstractConfigObject resolveSubstitutions(final SubstitutionResolver resolver,
final int depth,
final ConfigResolveOptions options) {
if (resolveStatus() == ResolveStatus.RESOLVED)
return this;
return modify(new Modifier() {
@Override
public AbstractConfigValue modifyChild(AbstractConfigValue v) {
return resolver.resolve(v, depth, options);
}
}, ResolveStatus.RESOLVED);
}
abstract AbstractConfigObject relativized(final Path prefix);
@Override
AbstractConfigObject relativized(final Path prefix) {
return modify(new Modifier() {
@Override
public AbstractConfigValue modifyChild(AbstractConfigValue v) {
return v.relativized(prefix);
}
}, resolveStatus());
}
public abstract AbstractConfigValue get(Object key);
@Override
public AbstractConfigValue get(Object key) {
if (key instanceof String)
return peek((String) key);
else
return null;
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
if (isEmpty()) {
sb.append("{}");
} else {
sb.append("{");
if (formatted)
sb.append('\n');
for (String k : keySet()) {
AbstractConfigValue v = peek(k);
if (formatted) {
indent(sb, indent + 1);
sb.append("# ");
sb.append(v.origin().description());
sb.append("\n");
for (String comment : v.origin().comments()) {
indent(sb, indent + 1);
sb.append("# ");
sb.append(comment);
sb.append("\n");
}
indent(sb, indent + 1);
}
v.render(sb, indent + 1, k, formatted);
sb.append(",");
if (formatted)
sb.append('\n');
}
// chop comma or newline
sb.setLength(sb.length() - 1);
if (formatted) {
sb.setLength(sb.length() - 1); // also chop comma
sb.append("\n"); // put a newline back
indent(sb, indent);
}
sb.append("}");
}
}
private static boolean mapEquals(Map<String, ConfigValue> a,
Map<String, ConfigValue> b) {
Set<String> aKeys = a.keySet();
Set<String> bKeys = b.keySet();
if (!aKeys.equals(bKeys))
return false;
for (String key : aKeys) {
if (!a.get(key).equals(b.get(key)))
return false;
}
return true;
}
private static int mapHash(Map<String, ConfigValue> m) {
// the keys have to be sorted, otherwise we could be equal
// to another map but have a different hashcode.
List<String> keys = new ArrayList<String>();
keys.addAll(m.keySet());
Collections.sort(keys);
int valuesHash = 0;
for (String k : keys) {
valuesHash += m.get(k).hashCode();
}
return 41 * (41 + keys.hashCode()) + valuesHash;
}
@Override
protected boolean canEqual(Object other) {
return other instanceof ConfigObject;
}
@Override
public boolean equals(Object other) {
// note that "origin" is deliberately NOT part of equality.
// neither are other "extras" like ignoresFallbacks or resolve status.
if (other instanceof ConfigObject) {
// optimization to avoid unwrapped() for two ConfigObject,
// which is what AbstractConfigValue does.
return canEqual(other) && mapEquals(this, ((ConfigObject) other));
} else {
return false;
}
}
@Override
public int hashCode() {
// note that "origin" is deliberately NOT part of equality
// neither are other "extras" like ignoresFallbacks or resolve status.
return mapHash(this);
}
protected abstract void render(StringBuilder sb, int indent, boolean formatted);
private static UnsupportedOperationException weAreImmutable(String method) {
return new UnsupportedOperationException(
"ConfigObject is immutable, you can't call Map.'" + method
+ "'");
return new UnsupportedOperationException("ConfigObject is immutable, you can't call Map."
+ method);
}
@Override

View File

@ -34,7 +34,62 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
}
/**
* Called only by SubstitutionResolver object.
* This exception means that a value is inherently not resolveable, for
* example because there's a cycle in the substitutions. That's different
* from a ConfigException.NotResolved which just means it hasn't been
* resolved. This is a checked exception since it's internal to the library
* and we want to be sure we handle it before passing it out to public API.
*/
static final class NotPossibleToResolve extends Exception {
private static final long serialVersionUID = 1L;
ConfigOrigin origin;
String path;
NotPossibleToResolve(String message) {
super(message);
this.origin = null;
this.path = null;
}
// use this constructor ONLY if you know the right origin and path
// to describe the problem.
NotPossibleToResolve(ConfigOrigin origin, String path, String message) {
this(origin, path, message, null);
}
NotPossibleToResolve(ConfigOrigin origin, String path, String message, Throwable cause) {
super(message, cause);
this.origin = origin;
this.path = path;
}
ConfigException exportException(ConfigOrigin outerOrigin, String outerPath) {
ConfigOrigin o = origin != null ? origin : outerOrigin;
String p = path != null ? path : outerPath;
if (p == null)
path = "";
if (o != null)
return new ConfigException.BadValue(o, p, getMessage(), this);
else
return new ConfigException.BadValue(p, getMessage(), this);
}
}
// thrown if a full rather than partial resolve is needed
static final class NeedsFullResolve extends Exception {
private static final long serialVersionUID = 1L;
NeedsFullResolve(String message) {
super(message);
}
}
/**
* Called only by SubstitutionResolver object. The "restrict to child"
* parameter is to avoid unnecessary cycles as a side effect (any sibling of
* the object we want to follow could cause a cycle, not just the object we
* want to follow, otherwise).
*
* @param resolver
* the resolver doing the resolving
@ -43,11 +98,13 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
* one
* @param options
* whether to look at system props and env vars
* @param restrictToChildOrNull
* if non-null, only recurse into this child path
* @return a new value if there were changes, or this if no changes
*/
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver,
int depth,
ConfigResolveOptions options) {
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver, int depth,
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve,
NeedsFullResolve {
return this;
}
@ -72,7 +129,25 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
}
protected interface Modifier {
AbstractConfigValue modifyChild(AbstractConfigValue v);
// keyOrNull is null for non-objects
AbstractConfigValue modifyChildMayThrow(String keyOrNull, AbstractConfigValue v)
throws Exception;
}
protected abstract class NoExceptionsModifier implements Modifier {
@Override
public final AbstractConfigValue modifyChildMayThrow(String keyOrNull, AbstractConfigValue v)
throws Exception {
try {
return modifyChild(keyOrNull, v);
} catch (RuntimeException e) {
throw e;
} catch(Exception e) {
throw new ConfigException.BugOrBroken("Unexpected exception", e);
}
}
abstract AbstractConfigValue modifyChild(String keyOrNull, AbstractConfigValue v);
}
@Override

View File

@ -64,22 +64,25 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
}
@Override
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver,
int depth, ConfigResolveOptions options) {
return resolveSubstitutions(stack, resolver, depth, options);
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver, int depth,
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve,
NeedsFullResolve {
return resolveSubstitutions(stack, resolver, depth, options, restrictToChildOrNull);
}
// static method also used by ConfigDelayedMergeObject
static AbstractConfigValue resolveSubstitutions(
List<AbstractConfigValue> stack, SubstitutionResolver resolver,
int depth, ConfigResolveOptions options) {
static AbstractConfigValue resolveSubstitutions(List<AbstractConfigValue> stack,
SubstitutionResolver resolver, int depth, ConfigResolveOptions options,
Path restrictToChildOrNull) throws NotPossibleToResolve, NeedsFullResolve {
// to resolve substitutions, we need to recursively resolve
// the stack of stuff to merge, and merge the stack so
// we won't be a delayed merge anymore.
// we won't be a delayed merge anymore. If restrictToChildOrNull
// is non-null, we may remain a delayed merge though.
AbstractConfigValue merged = null;
for (AbstractConfigValue v : stack) {
AbstractConfigValue resolved = resolver.resolve(v, depth, options);
AbstractConfigValue resolved = resolver.resolve(v, depth, options,
restrictToChildOrNull);
if (resolved != null) {
if (merged == null)
merged = resolved;

View File

@ -61,11 +61,11 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements
}
@Override
AbstractConfigObject resolveSubstitutions(SubstitutionResolver resolver,
int depth, ConfigResolveOptions options) {
AbstractConfigValue merged = ConfigDelayedMerge.resolveSubstitutions(
stack, resolver, depth,
options);
AbstractConfigObject resolveSubstitutions(SubstitutionResolver resolver, int depth,
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve,
NeedsFullResolve {
AbstractConfigValue merged = ConfigDelayedMerge.resolveSubstitutions(stack, resolver,
depth, options, restrictToChildOrNull);
if (merged instanceof AbstractConfigObject) {
return (AbstractConfigObject) merged;
} else {
@ -171,6 +171,11 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements
ConfigDelayedMerge.render(stack, sb, indent, atKey, formatted);
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
render(sb, indent, null, formatted);
}
private static ConfigException notResolved() {
return new ConfigException.NotResolved(
"bug: this object has not had substitutions resolved, so can't be used");
@ -181,6 +186,11 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements
throw notResolved();
}
@Override
public AbstractConfigValue get(Object key) {
throw notResolved();
}
@Override
public boolean containsKey(Object key) {
throw notResolved();
@ -217,8 +227,73 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements
}
@Override
protected AbstractConfigValue peek(String key) {
throw notResolved();
protected AbstractConfigValue attemptPeekWithPartialResolve(String key) throws NeedsFullResolve {
// a partial resolve of a ConfigDelayedMergeObject always results in a
// SimpleConfigObject because all the substitutions in the stack get
// resolved in order to look up the partial.
// So we know here that we have not been resolved at all even
// partially.
// Given that, all this code is probably gratuitous, since the app code
// is likely broken. But in general we only throw NotResolved if you try
// to touch the exact key that isn't resolved, so this is in that
// spirit.
// this function should never return null; if we know a value doesn't
// exist, then there would be no reason for the merge to be delayed
// (i.e. as long as some stuff is unmerged, the value may be non-null).
// we'll be able to return a key if we have a value that ignores
// fallbacks, prior to any unmergeable values.
for (AbstractConfigValue layer : stack) {
if (layer instanceof AbstractConfigObject) {
AbstractConfigObject objectLayer = (AbstractConfigObject) layer;
AbstractConfigValue v = objectLayer.attemptPeekWithPartialResolve(key);
if (v != null) {
if (v.ignoresFallbacks()) {
// we know we won't need to merge anything in to this
// value
return v;
} else {
// we can't return this value because we know there are
// unmergeable values later in the stack that may
// contain values that need to be merged with this
// value. we'll throw the exception when we get to those
// unmergeable values, so continue here.
continue;
}
} else if (layer instanceof Unmergeable) {
// an unmergeable object (which would be another
// ConfigDelayedMergeObject) can't know that a key is
// missing, so it can't return null; it can only return a
// value or throw NotPossibleToResolve
throw new ConfigException.BugOrBroken(
"should not be reached: unmergeable object returned null value");
} else {
// a non-unmergeable AbstractConfigObject that returned null
// for the key in question is not relevant, we can keep
// looking for a value.
continue;
}
} else if (layer instanceof Unmergeable) {
throw new NeedsFullResolve("Key '" + key + "' is not available at '"
+ origin().description() + "' because value at '"
+ layer.origin().description()
+ "' has not been resolved and may turn out to contain '" + key + "'."
+ " Be sure to Config.resolve() before using a config object.");
} else {
// non-object, but not unresolved, like an integer or something.
// has no children so the one we're after won't be in it.
// this should always be overridden by an object though so
// ideally we never build a stack that would have this in it.
continue;
}
}
// If we get here, then we never found an unmergeable which means
// the ConfigDelayedMergeObject should not have existed. some
// invariant was violated.
throw new ConfigException.BugOrBroken(
"Delayed merge stack does not contain any unmergeable values");
}
// This ridiculous hack is because some JDK versions apparently can't

View File

@ -127,31 +127,27 @@ final class ConfigSubstitution extends AbstractConfigValue implements
// larger than anyone would ever want
private static final int MAX_DEPTH = 100;
private ConfigValue findInObject(AbstractConfigObject root,
private AbstractConfigValue findInObject(AbstractConfigObject root,
SubstitutionResolver resolver, /* null if we should not have refs */
Path subst, int depth, ConfigResolveOptions options) {
Path subst, int depth, ConfigResolveOptions options) throws NotPossibleToResolve,
NeedsFullResolve {
if (depth > MAX_DEPTH) {
throw new ConfigException.BadValue(origin(), subst.render(),
"Substitution ${" + subst.render()
+ "} is part of a cycle of substitutions");
throw new NotPossibleToResolve(origin(), subst.render(), "Substitution ${"
+ subst.render() + "} is part of a cycle of substitutions");
}
ConfigValue result = root.peekPath(subst, resolver, depth, options);
if (result instanceof ConfigSubstitution) {
throw new ConfigException.BugOrBroken(
"peek or peekPath returned an unresolved substitution");
}
AbstractConfigValue result = root.peekPath(subst, resolver, depth, options);
return result;
}
private ConfigValue resolve(SubstitutionResolver resolver, SubstitutionExpression subst,
int depth, ConfigResolveOptions options) {
private AbstractConfigValue resolve(SubstitutionResolver resolver,
SubstitutionExpression subst, int depth, ConfigResolveOptions options,
Path restrictToChildOrNull) throws NotPossibleToResolve, NeedsFullResolve {
// First we look up the full path, which means relative to the
// included file if we were not a root file
ConfigValue result = findInObject(resolver.root(), resolver, subst.path(),
depth, options);
AbstractConfigValue result = findInObject(resolver.root(), resolver, subst.path(), depth,
options);
if (result == null) {
// Then we want to check relative to the root file. We don't
@ -169,11 +165,15 @@ final class ConfigSubstitution extends AbstractConfigValue implements
}
}
if (result != null) {
result = resolver.resolve(result, depth, options, restrictToChildOrNull);
}
return result;
}
private ConfigValue resolve(SubstitutionResolver resolver, int depth,
ConfigResolveOptions options) {
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve {
if (pieces.size() > 1) {
// need to concat everything into a string
StringBuilder sb = new StringBuilder();
@ -182,7 +182,15 @@ final class ConfigSubstitution extends AbstractConfigValue implements
sb.append((String) p);
} else {
SubstitutionExpression exp = (SubstitutionExpression) p;
ConfigValue v = resolve(resolver, exp, depth, options);
ConfigValue v;
try {
// to concat into a string we have to do a full resolve,
// so don't pass along restrictToChildOrNull
v = resolve(resolver, exp, depth, options, null);
} catch (NeedsFullResolve e) {
throw new NotPossibleToResolve(null, exp.path().render(),
"Some kind of loop or interdependency prevents resolving " + exp, e);
}
if (v == null) {
if (exp.optional()) {
@ -210,7 +218,13 @@ final class ConfigSubstitution extends AbstractConfigValue implements
throw new ConfigException.BugOrBroken(
"ConfigSubstitution should never contain a single String piece");
SubstitutionExpression exp = (SubstitutionExpression) pieces.get(0);
ConfigValue v = resolve(resolver, exp, depth, options);
ConfigValue v;
try {
v = resolve(resolver, exp, depth, options, restrictToChildOrNull);
} catch (NeedsFullResolve e) {
throw new NotPossibleToResolve(null, exp.path().render(),
"Some kind of loop or interdependency prevents resolving " + exp, e);
}
if (v == null && !exp.optional()) {
throw new ConfigException.UnresolvedSubstitution(origin(), exp.toString());
}
@ -219,13 +233,12 @@ final class ConfigSubstitution extends AbstractConfigValue implements
}
@Override
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver,
int depth,
ConfigResolveOptions options) {
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver, int depth,
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve {
// only ConfigSubstitution adds to depth here, because the depth
// is the substitution depth not the recursion depth
AbstractConfigValue resolved = (AbstractConfigValue) resolve(resolver,
depth + 1, options);
// is the substitution depth not the recursion depth.
AbstractConfigValue resolved = (AbstractConfigValue) resolve(resolver, depth + 1, options,
restrictToChildOrNull);
return resolved;
}

View File

@ -22,6 +22,8 @@ import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
import com.typesafe.config.impl.AbstractConfigValue.NeedsFullResolve;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
/**
* One thing to keep in mind in the future: as Collection-like APIs are added
@ -56,8 +58,9 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
@Override
public SimpleConfig resolve(ConfigResolveOptions options) {
AbstractConfigValue resolved = SubstitutionResolver.resolve(object,
AbstractConfigValue resolved = SubstitutionResolver.resolveWithExternalExceptions(object,
object, options);
if (resolved == object)
return this;
else
@ -68,7 +71,15 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
@Override
public boolean hasPath(String pathExpression) {
Path path = Path.newPath(pathExpression);
ConfigValue peeked = object.peekPath(path, null, 0, null);
ConfigValue peeked;
try {
peeked = object.peekPath(path, null, 0, null);
} catch (NotPossibleToResolve e) {
throw e.exportException(origin(), pathExpression);
} catch (NeedsFullResolve e) {
throw new ConfigException.NotResolved(origin().description() + ": " + pathExpression
+ ": Have to resolve() the Config before using hasPath() here");
}
return peeked != null && peeked.valueType() != ConfigValueType.NULL;
}
@ -110,7 +121,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
static private AbstractConfigValue findKey(AbstractConfigObject self,
String key, ConfigValueType expected, String originalPath) {
AbstractConfigValue v = self.peek(key);
AbstractConfigValue v = self.peekAssumingResolved(key, originalPath);
if (v == null)
throw new ConfigException.Missing(originalPath);
@ -656,7 +667,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
}
private AbstractConfigValue peekPath(Path path) {
return root().peekPath(path);
return root().peekPathWithExternalExceptions(path);
}
private static void addProblem(List<ConfigException.ValidationProblem> accumulator, Path path,

View File

@ -10,6 +10,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions;
@ -54,13 +55,23 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
return ResolveStatus.fromBoolean(resolved);
}
private SimpleConfigList modify(Modifier modifier,
ResolveStatus newResolveStatus) {
private SimpleConfigList modify(NoExceptionsModifier modifier, ResolveStatus newResolveStatus) {
try {
return modifyMayThrow(modifier, newResolveStatus);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ConfigException.BugOrBroken("unexpected checked exception", e);
}
}
private SimpleConfigList modifyMayThrow(Modifier modifier, ResolveStatus newResolveStatus)
throws Exception {
// lazy-create for optimization
List<AbstractConfigValue> changed = null;
int i = 0;
for (AbstractConfigValue v : value) {
AbstractConfigValue modified = modifier.modifyChild(v);
AbstractConfigValue modified = modifier.modifyChildMayThrow(null /* key */, v);
// lazy-create the new list if required
if (changed == null && modified != v) {
@ -88,25 +99,43 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
}
@Override
SimpleConfigList resolveSubstitutions(final SubstitutionResolver resolver,
final int depth, final ConfigResolveOptions options) {
SimpleConfigList resolveSubstitutions(final SubstitutionResolver resolver, final int depth,
final ConfigResolveOptions options, Path restrictToChildOrNull)
throws NotPossibleToResolve, NeedsFullResolve {
if (resolved)
return this;
return modify(new Modifier() {
@Override
public AbstractConfigValue modifyChild(AbstractConfigValue v) {
return resolver.resolve(v, depth, options);
}
if (restrictToChildOrNull != null) {
// if a list restricts to a child path, then it has no child paths,
// so nothing to do.
return this;
} else {
try {
return modifyMayThrow(new Modifier() {
@Override
public AbstractConfigValue modifyChildMayThrow(String key, AbstractConfigValue v)
throws NotPossibleToResolve, NeedsFullResolve {
return resolver.resolve(v, depth, options, null /* restrictToChild */);
}
}, ResolveStatus.RESOLVED);
}, ResolveStatus.RESOLVED);
} catch (NotPossibleToResolve e) {
throw e;
} catch (NeedsFullResolve e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ConfigException.BugOrBroken("unexpected checked exception", e);
}
}
}
@Override
SimpleConfigList relativized(final Path prefix) {
return modify(new Modifier() {
return modify(new NoExceptionsModifier() {
@Override
public AbstractConfigValue modifyChild(AbstractConfigValue v) {
public AbstractConfigValue modifyChild(String key, AbstractConfigValue v) {
return v.relativized(prefix);
}

View File

@ -4,15 +4,19 @@
package com.typesafe.config.impl;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
final class SimpleConfigObject extends AbstractConfigObject {
@ -34,6 +38,15 @@ final class SimpleConfigObject extends AbstractConfigObject {
this.value = value;
this.resolved = status == ResolveStatus.RESOLVED;
this.ignoresFallbacks = ignoresFallbacks;
// Kind of an expensive debug check. Comment out?
boolean allResolved = true;
for (AbstractConfigValue v : value.values()) {
if (v.resolveStatus() != ResolveStatus.RESOLVED)
allResolved = false;
}
if (this.resolved != allResolved)
throw new ConfigException.BugOrBroken("Wrong resolved status on " + this);
}
SimpleConfigObject(ConfigOrigin origin,
@ -119,7 +132,7 @@ final class SimpleConfigObject extends AbstractConfigObject {
}
@Override
protected AbstractConfigValue peek(String key) {
protected AbstractConfigValue attemptPeekWithPartialResolve(String key) {
return value.get(key);
}
@ -148,6 +161,262 @@ final class SimpleConfigObject extends AbstractConfigObject {
return m;
}
@Override
protected SimpleConfigObject mergedWithObject(AbstractConfigObject abstractFallback) {
if (ignoresFallbacks())
throw new ConfigException.BugOrBroken("should not be reached");
if (!(abstractFallback instanceof SimpleConfigObject)) {
throw new ConfigException.BugOrBroken(
"should not be reached (merging non-SimpleConfigObject)");
}
SimpleConfigObject fallback = (SimpleConfigObject) abstractFallback;
boolean changed = false;
boolean allResolved = true;
Map<String, AbstractConfigValue> merged = new HashMap<String, AbstractConfigValue>();
Set<String> allKeys = new HashSet<String>();
allKeys.addAll(this.keySet());
allKeys.addAll(fallback.keySet());
for (String key : allKeys) {
AbstractConfigValue first = this.value.get(key);
AbstractConfigValue second = fallback.value.get(key);
AbstractConfigValue kept;
if (first == null)
kept = second;
else if (second == null)
kept = first;
else
kept = first.withFallback(second);
merged.put(key, kept);
if (first != kept)
changed = true;
if (kept.resolveStatus() == ResolveStatus.UNRESOLVED)
allResolved = false;
}
ResolveStatus newResolveStatus = ResolveStatus.fromBoolean(allResolved);
boolean newIgnoresFallbacks = fallback.ignoresFallbacks();
if (changed)
return new SimpleConfigObject(mergeOrigins(this, fallback), merged, newResolveStatus,
newIgnoresFallbacks);
else if (newResolveStatus != resolveStatus() || newIgnoresFallbacks != ignoresFallbacks())
return newCopy(newResolveStatus, newIgnoresFallbacks, origin());
else
return this;
}
private SimpleConfigObject modify(NoExceptionsModifier modifier) {
try {
return modifyMayThrow(modifier);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ConfigException.BugOrBroken("unexpected checked exception", e);
}
}
private SimpleConfigObject modifyMayThrow(Modifier modifier) throws Exception {
Map<String, AbstractConfigValue> changes = null;
for (String k : keySet()) {
AbstractConfigValue v = value.get(k);
// "modified" may be null, which means remove the child;
// to do that we put null in the "changes" map.
AbstractConfigValue modified = modifier.modifyChildMayThrow(k, v);
if (modified != v) {
if (changes == null)
changes = new HashMap<String, AbstractConfigValue>();
changes.put(k, modified);
}
}
if (changes == null) {
return newCopy(resolveStatus(), ignoresFallbacks(), origin());
} else {
Map<String, AbstractConfigValue> modified = new HashMap<String, AbstractConfigValue>();
boolean sawUnresolved = false;
for (String k : keySet()) {
if (changes.containsKey(k)) {
AbstractConfigValue newValue = changes.get(k);
if (newValue != null) {
modified.put(k, newValue);
if (newValue.resolveStatus() == ResolveStatus.UNRESOLVED)
sawUnresolved = true;
} else {
// remove this child; don't put it in the new map.
}
} else {
AbstractConfigValue newValue = value.get(k);
modified.put(k, newValue);
if (newValue.resolveStatus() == ResolveStatus.UNRESOLVED)
sawUnresolved = true;
}
}
return new SimpleConfigObject(origin(), modified,
sawUnresolved ? ResolveStatus.UNRESOLVED : ResolveStatus.RESOLVED,
ignoresFallbacks());
}
}
@Override
AbstractConfigObject resolveSubstitutions(final SubstitutionResolver resolver, final int depth,
final ConfigResolveOptions options, final Path restrictToChildOrNull)
throws NotPossibleToResolve, NeedsFullResolve {
if (resolveStatus() == ResolveStatus.RESOLVED)
return this;
try {
return modifyMayThrow(new Modifier() {
@Override
public AbstractConfigValue modifyChildMayThrow(String key, AbstractConfigValue v)
throws NotPossibleToResolve, NeedsFullResolve {
if (restrictToChildOrNull != null) {
if (key.equals(restrictToChildOrNull.first())) {
Path remainder = restrictToChildOrNull.remainder();
if (remainder != null) {
return resolver.resolve(v, depth, options, remainder);
} else {
// we don't want to resolve the leaf child.
return v;
}
} else {
// not in the restrictToChild path
return v;
}
} else {
// no restrictToChild, resolve everything
return resolver.resolve(v, depth, options, null);
}
}
});
} catch (NotPossibleToResolve e) {
throw e;
} catch (NeedsFullResolve e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ConfigException.BugOrBroken("unexpected checked exception", e);
}
}
@Override
SimpleConfigObject relativized(final Path prefix) {
return modify(new NoExceptionsModifier() {
@Override
public AbstractConfigValue modifyChild(String key, AbstractConfigValue v) {
return v.relativized(prefix);
}
});
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
if (isEmpty()) {
sb.append("{}");
} else {
sb.append("{");
if (formatted)
sb.append('\n');
for (String k : keySet()) {
AbstractConfigValue v;
v = value.get(k);
if (formatted) {
indent(sb, indent + 1);
sb.append("# ");
sb.append(v.origin().description());
sb.append("\n");
for (String comment : v.origin().comments()) {
indent(sb, indent + 1);
sb.append("# ");
sb.append(comment);
sb.append("\n");
}
indent(sb, indent + 1);
}
v.render(sb, indent + 1, k, formatted);
sb.append(",");
if (formatted)
sb.append('\n');
}
// chop comma or newline
sb.setLength(sb.length() - 1);
if (formatted) {
sb.setLength(sb.length() - 1); // also chop comma
sb.append("\n"); // put a newline back
indent(sb, indent);
}
sb.append("}");
}
}
@Override
public AbstractConfigValue get(Object key) {
return value.get(key);
}
private static boolean mapEquals(Map<String, ConfigValue> a, Map<String, ConfigValue> b) {
Set<String> aKeys = a.keySet();
Set<String> bKeys = b.keySet();
if (!aKeys.equals(bKeys))
return false;
for (String key : aKeys) {
if (!a.get(key).equals(b.get(key)))
return false;
}
return true;
}
private static int mapHash(Map<String, ConfigValue> m) {
// the keys have to be sorted, otherwise we could be equal
// to another map but have a different hashcode.
List<String> keys = new ArrayList<String>();
keys.addAll(m.keySet());
Collections.sort(keys);
int valuesHash = 0;
for (String k : keys) {
valuesHash += m.get(k).hashCode();
}
return 41 * (41 + keys.hashCode()) + valuesHash;
}
@Override
protected boolean canEqual(Object other) {
return other instanceof ConfigObject;
}
@Override
public boolean equals(Object other) {
// note that "origin" is deliberately NOT part of equality.
// neither are other "extras" like ignoresFallbacks or resolve status.
if (other instanceof ConfigObject) {
// optimization to avoid unwrapped() for two ConfigObject,
// which is what AbstractConfigValue does.
return canEqual(other) && mapEquals(this, ((ConfigObject) other));
} else {
return false;
}
}
@Override
public int hashCode() {
// note that "origin" is deliberately NOT part of equality
// neither are other "extras" like ignoresFallbacks or resolve status.
return mapHash(this);
}
@Override
public boolean containsKey(Object key) {
return value.containsKey(key);

View File

@ -3,11 +3,13 @@
*/
package com.typesafe.config.impl;
import java.util.IdentityHashMap;
import java.util.HashMap;
import java.util.Map;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.impl.AbstractConfigValue.NeedsFullResolve;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
/**
* This exists because we have to memoize resolved substitutions as we go
@ -15,30 +17,99 @@ import com.typesafe.config.ConfigResolveOptions;
* of values or whole trees of values as we follow chains of substitutions.
*/
final class SubstitutionResolver {
private final class MemoKey {
MemoKey(AbstractConfigValue value, Path restrictToChildOrNull) {
this.value = value;
this.restrictToChildOrNull = restrictToChildOrNull;
}
final private AbstractConfigValue value;
final private Path restrictToChildOrNull;
@Override
public final int hashCode() {
int h = System.identityHashCode(value);
if (restrictToChildOrNull != null) {
return h + 41 * (41 + restrictToChildOrNull.hashCode());
} else {
return h;
}
}
@Override
public final boolean equals(Object other) {
if (other instanceof MemoKey) {
MemoKey o = (MemoKey) other;
if (o.value != this.value)
return false;
else if (o.restrictToChildOrNull == this.restrictToChildOrNull)
return true;
else if (o.restrictToChildOrNull == null || this.restrictToChildOrNull == null)
return false;
else
return o.restrictToChildOrNull.equals(this.restrictToChildOrNull);
} else {
return false;
}
}
}
final private AbstractConfigObject root;
// note that we can resolve things to undefined (represented as Java null,
// rather than ConfigNull) so this map can have null values.
final private Map<AbstractConfigValue, AbstractConfigValue> memos;
final private Map<MemoKey, AbstractConfigValue> memos;
SubstitutionResolver(AbstractConfigObject root) {
this.root = root;
// note: the memoization is by object identity, not object value
this.memos = new IdentityHashMap<AbstractConfigValue, AbstractConfigValue>();
this.memos = new HashMap<MemoKey, AbstractConfigValue>();
}
AbstractConfigValue resolve(AbstractConfigValue original, int depth,
ConfigResolveOptions options) {
if (memos.containsKey(original)) {
return memos.get(original);
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve,
NeedsFullResolve {
// a fully-resolved (no restrictToChild) object can satisfy a request
// for a restricted object, so always check that first.
final MemoKey fullKey = new MemoKey(original, null);
MemoKey restrictedKey = null;
AbstractConfigValue cached = memos.get(fullKey);
// but if there was no fully-resolved object cached, we'll only
// compute the restrictToChild object so use a more limited
// memo key
if (cached == null && restrictToChildOrNull != null) {
restrictedKey = new MemoKey(original, restrictToChildOrNull);
cached = memos.get(restrictedKey);
}
if (cached != null) {
return cached;
} else {
AbstractConfigValue resolved = original.resolveSubstitutions(this,
depth, options);
if (resolved != null) {
if (resolved.resolveStatus() != ResolveStatus.RESOLVED)
AbstractConfigValue resolved = original.resolveSubstitutions(this, depth, options,
restrictToChildOrNull);
if (resolved == null || resolved.resolveStatus() == ResolveStatus.RESOLVED) {
// if the resolved object is fully resolved by resolving only
// the restrictToChildOrNull, then it can be cached under
// fullKey since the child we were restricted to turned out to
// be the only unresolved thing.
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.
if (restrictToChildOrNull == null) {
throw new ConfigException.BugOrBroken(
"resolveSubstitutions() did not give us a resolved object");
} else {
if (restrictedKey == null) {
throw new ConfigException.BugOrBroken(
"restrictedKey should not be null here");
}
memos.put(restrictedKey, resolved);
}
}
memos.put(original, resolved);
return resolved;
}
}
@ -47,9 +118,23 @@ final class SubstitutionResolver {
return this.root;
}
static AbstractConfigValue resolve(AbstractConfigValue value,
static AbstractConfigValue resolve(AbstractConfigValue value, AbstractConfigObject root,
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve,
NeedsFullResolve {
SubstitutionResolver resolver = new SubstitutionResolver(root);
return resolver.resolve(value, 0, options, restrictToChildOrNull);
}
static AbstractConfigValue resolveWithExternalExceptions(AbstractConfigValue value,
AbstractConfigObject root, ConfigResolveOptions options) {
SubstitutionResolver resolver = new SubstitutionResolver(root);
return resolver.resolve(value, 0, options);
try {
return resolver.resolve(value, 0, options, null /* restrictToChild */);
} catch (NotPossibleToResolve e) {
throw e.exportException(value.origin(), null);
} catch (NeedsFullResolve e) {
throw new ConfigException.NotResolved(value.origin().description()
+ ": Must resolve() config object before use", e);
}
}
}

View File

@ -31,7 +31,7 @@ class ConfParserTest extends TestUtils {
// interpolating arrays into strings
tree match {
case obj: AbstractConfigObject =>
SubstitutionResolver.resolve(tree, obj, ConfigResolveOptions.noSystem())
SubstitutionResolver.resolveWithExternalExceptions(tree, obj, ConfigResolveOptions.noSystem())
case _ =>
tree
}

View File

@ -15,20 +15,20 @@ class ConfigSubstitutionTest extends TestUtils {
private def resolveWithoutFallbacks(v: AbstractConfigObject) = {
val options = ConfigResolveOptions.noSystem()
SubstitutionResolver.resolve(v, v, options).asInstanceOf[AbstractConfigObject].toConfig
SubstitutionResolver.resolveWithExternalExceptions(v, v, options).asInstanceOf[AbstractConfigObject].toConfig
}
private def resolveWithoutFallbacks(s: ConfigSubstitution, root: AbstractConfigObject) = {
val options = ConfigResolveOptions.noSystem()
SubstitutionResolver.resolve(s, root, options)
SubstitutionResolver.resolveWithExternalExceptions(s, root, options)
}
private def resolve(v: AbstractConfigObject) = {
val options = ConfigResolveOptions.defaults()
SubstitutionResolver.resolve(v, v, options).asInstanceOf[AbstractConfigObject].toConfig
SubstitutionResolver.resolveWithExternalExceptions(v, v, options).asInstanceOf[AbstractConfigObject].toConfig
}
private def resolve(s: ConfigSubstitution, root: AbstractConfigObject) = {
val options = ConfigResolveOptions.defaults()
SubstitutionResolver.resolve(s, root, options)
SubstitutionResolver.resolveWithExternalExceptions(s, root, options)
}
private val simpleObject = {
@ -269,7 +269,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test
def avoidDelayedMergeObjectResolveProblem1() {
assertTrue(delayedMergeObjectResolveProblem1.peek("item1").isInstanceOf[ConfigDelayedMergeObject])
assertTrue(delayedMergeObjectResolveProblem1.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMergeObject])
val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem1)
@ -296,7 +296,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test
def avoidDelayedMergeObjectResolveProblem2() {
assertTrue(delayedMergeObjectResolveProblem2.peek("item1").isInstanceOf[ConfigDelayedMergeObject])
assertTrue(delayedMergeObjectResolveProblem2.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMergeObject])
val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem2)
@ -325,7 +325,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test
def avoidDelayedMergeObjectResolveProblem3() {
assertTrue(delayedMergeObjectResolveProblem3.peek("item1").isInstanceOf[ConfigDelayedMergeObject])
assertTrue(delayedMergeObjectResolveProblem3.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMergeObject])
val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem3)
@ -354,7 +354,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test
def avoidDelayedMergeObjectResolveProblem4() {
// in this case we have a ConfigDelayedMerge not a ConfigDelayedMergeObject
assertTrue(delayedMergeObjectResolveProblem4.peek("item1").isInstanceOf[ConfigDelayedMerge])
assertTrue(delayedMergeObjectResolveProblem4.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMerge])
val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem4)
@ -381,7 +381,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test
def avoidDelayedMergeObjectResolveProblem5() {
// in this case we have a ConfigDelayedMerge not a ConfigDelayedMergeObject
assertTrue(delayedMergeObjectResolveProblem5.peek("item1").isInstanceOf[ConfigDelayedMerge])
assertTrue(delayedMergeObjectResolveProblem5.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMerge])
val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem5)
@ -390,6 +390,101 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(2, resolved.getInt("defaults.a"))
}
private val delayedMergeObjectResolveProblem6 = {
parseObject("""
z = 15
defaults-defaults-defaults {
m = ${z}
n.o.p = ${z}
}
defaults-defaults {
x = 10
y = 11
asdf = ${z}
}
defaults {
a = 1
b = 2
}
defaults-alias = ${defaults}
// make item1 into a ConfigDelayedMergeObject several layers deep
// that will NOT become resolved just because we resolve one path
// through it.
item1 = 345
item1 = ${?NONEXISTENT}
item1 = ${defaults-defaults-defaults}
item1 = {}
item1 = ${defaults-defaults}
item1 = ${defaults-alias}
item1 = ${defaults}
item1.b = { c : 43 }
item1.xyz = 101
// be sure we can resolve a substitution to a value in
// a delayed-merge object.
item2.b = ${item1.b}
""")
}
@Test
def avoidDelayedMergeObjectResolveProblem6() {
assertTrue(delayedMergeObjectResolveProblem6.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMergeObject])
// should be able to attemptPeekWithPartialResolve() a known non-object without resolving
assertEquals(101, delayedMergeObjectResolveProblem6.toConfig().getObject("item1").attemptPeekWithPartialResolve("xyz").unwrapped())
val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem6)
assertEquals(parseObject("{ c : 43 }"), resolved.getObject("item1.b"))
assertEquals(43, resolved.getInt("item1.b.c"))
assertEquals(43, resolved.getInt("item2.b.c"))
assertEquals(15, resolved.getInt("item1.n.o.p"))
}
private val delayedMergeObjectWithKnownValue = {
parseObject("""
defaults {
a = 1
b = 2
}
// make item1 into a ConfigDelayedMergeObject
item1 = ${defaults}
// note that we'll resolve to a non-object value
// so item1.b will ignoreFallbacks and not depend on
// ${defaults}
item1.b = 3
""")
}
@Test
def fetchKnownValueFromDelayedMergeObject() {
assertTrue(delayedMergeObjectWithKnownValue.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMergeObject])
assertEquals(3, delayedMergeObjectWithKnownValue.toConfig.getConfig("item1").getInt("b"))
}
private val delayedMergeObjectNeedsFullResolve = {
parseObject("""
defaults {
a = 1
b = { c : 31 }
}
item1 = ${defaults}
// because b is an object, fetching it requires resolving ${defaults} above
// to see if there are more keys to merge with b.
item1.b = { c : 41 }
""")
}
@Test
def failToFetchFromDelayedMergeObjectNeedsFullResolve() {
assertTrue(delayedMergeObjectWithKnownValue.attemptPeekWithPartialResolve("item1").isInstanceOf[ConfigDelayedMergeObject])
val e = intercept[ConfigException.NotResolved] {
delayedMergeObjectNeedsFullResolve.toConfig().getObject("item1.b")
}
assertTrue(e.getMessage.contains("item1.b"))
}
@Test
def useRelativeToSameFileWhenRelativized() {
val child = parseObject("""foo=in child,bar=${foo}""")

View File

@ -20,11 +20,11 @@ import com.typesafe.config.ConfigMergeable
class ConfigTest extends TestUtils {
private def resolveNoSystem(v: AbstractConfigValue, root: AbstractConfigObject) = {
SubstitutionResolver.resolve(v, root, ConfigResolveOptions.noSystem())
SubstitutionResolver.resolveWithExternalExceptions(v, root, ConfigResolveOptions.noSystem())
}
private def resolveNoSystem(v: SimpleConfig, root: SimpleConfig) = {
SubstitutionResolver.resolve(v.root, root.root,
SubstitutionResolver.resolveWithExternalExceptions(v.root, root.root,
ConfigResolveOptions.noSystem()).asInstanceOf[AbstractConfigObject].toConfig
}

View File

@ -553,6 +553,7 @@ class ConfigValueTest extends TestUtils {
List[AbstractConfigValue](emptyObj, subst("a"), subst("b")).asJava)
assertEquals(ConfigValueType.OBJECT, dmo.valueType())
unresolved { dmo.unwrapped() }
unresolved { dmo.get("foo") }
unresolved { dmo.containsKey(null) }
unresolved { dmo.containsValue(null) }
unresolved { dmo.entrySet() }

View File

@ -34,7 +34,7 @@ class EquivalentsTest extends TestUtils {
// for purposes of these tests, substitutions are only
// against the same file's root, and without looking at
// system prop or env variable fallbacks.
SubstitutionResolver.resolve(v, v, ConfigResolveOptions.noSystem())
SubstitutionResolver.resolveWithExternalExceptions(v, v, ConfigResolveOptions.noSystem())
case v =>
v
}