Clean up code and semantics around self-referential handling

The basic idea in this patch is to unify on the "replacer"
(modifying the tree in which we lookup substitutions)
mechanism for detecting cycles.

Drop the "traverse" method, instead keeping a trace of expressions
we've passed through for nice error messages.

There is now only one internal checked exception possible,
NotPossibleToResolve which is thrown when a cycle is detected;
this checked exception is always caught by ConfigReference
so cannot "escape" from the library. The exception exists
because we want to get the traceString debug info out
of the spot that detected the cycle, if we didn't want that
debug info we could just return null as usual for undefined.

As part of simplifying this (which should also simplify the
spec), resolutions which require double-traverse of the same
reference are no longer supported:
   a=1, b=${a}, a=${b}

Also, cycles now always throw UnresolvedSubstitution rather
than BadValue. This was needed for consistency since
conceptually a single a=${a} is going to "look back" earlier
in the merge stack, discover there is no earlier value of a,
and fail; it should be the same exception as a=${a},a={b:1},
and in both cases referring to these cycles via ${?} should
hide the exception.
This commit is contained in:
Havoc Pennington 2012-04-05 09:39:39 -04:00
parent 006777c062
commit 683e72cbbe
15 changed files with 208 additions and 274 deletions

View File

@ -244,12 +244,12 @@ public abstract class ConfigException extends RuntimeException {
public static class UnresolvedSubstitution extends Parse { public static class UnresolvedSubstitution extends Parse {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public UnresolvedSubstitution(ConfigOrigin origin, String expression, Throwable cause) { public UnresolvedSubstitution(ConfigOrigin origin, String detail, Throwable cause) {
super(origin, "Could not resolve substitution to a value: " + expression, cause); super(origin, "Could not resolve substitution to a value: " + detail, cause);
} }
public UnresolvedSubstitution(ConfigOrigin origin, String expression) { public UnresolvedSubstitution(ConfigOrigin origin, String detail) {
this(origin, expression, null); this(origin, detail, null);
} }
} }

View File

@ -85,20 +85,22 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi
* resolver != null. * resolver != null.
* *
* @throws NotPossibleToResolve * @throws NotPossibleToResolve
* if context is not null and resolution fails
*/ */
protected AbstractConfigValue peekPath(Path path, ResolveContext context) throws NotPossibleToResolve { protected AbstractConfigValue peekPath(Path path, ResolveContext context) throws NotPossibleToResolve {
return peekPath(this, path, context); return peekPath(this, path, context);
} }
/** /**
* Looks up the path and throws public API exceptions (ConfigException). * Looks up the path. Doesn't do any resolution, will throw if any is
* Doesn't do any resolution, will throw if any is needed. * needed.
*/ */
AbstractConfigValue peekPathWithExternalExceptions(Path path) { AbstractConfigValue peekPath(Path path) {
try { try {
return peekPath(this, path, null); return peekPath(this, path, null);
} catch (NotPossibleToResolve e) { } catch (NotPossibleToResolve e) {
throw e.exportException(origin(), path.render()); throw new ConfigException.BugOrBroken(
"NotPossibleToResolve happened though we had no ResolveContext in peekPath");
} }
} }

View File

@ -33,53 +33,29 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
} }
/** /**
* This exception means that a value is inherently not resolveable, for * This exception means that a value is inherently not resolveable, at the
* example because there's a cycle in the substitutions. That's different * moment the only known cause is a cycle of substitutions. This is a
* from a ConfigException.NotResolved which just means it hasn't been * checked exception since it's internal to the library and we want to be
* resolved. This is a checked exception since it's internal to the library * sure we handle it before passing it out to public API. This is only
* and we want to be sure we handle it before passing it out to public API. * supposed to be thrown by the target of a cyclic reference and it's
* supposed to be caught by the ConfigReference looking up that reference,
* so it should be impossible for an outermost resolve() to throw this.
*
* Contrast with ConfigException.NotResolved which just means nobody called
* resolve().
*/ */
static class NotPossibleToResolve extends Exception { static class NotPossibleToResolve extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
ConfigOrigin origin; final private String traceString;
String path;
NotPossibleToResolve(String message) { NotPossibleToResolve(ResolveContext context) {
super(message); super("was not possible to resolve");
this.origin = null; this.traceString = context.traceString();
this.path = null;
} }
// use this constructor ONLY if you know the right origin and path String traceString() {
// to describe the problem. return traceString;
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);
}
}
static final class SelfReferential extends NotPossibleToResolve {
private static final long serialVersionUID = 1L;
SelfReferential(ConfigOrigin origin, String path) {
super(origin, path, "Substitution ${" + path + "} is part of a cycle of substitutions");
} }
} }

View File

@ -102,20 +102,13 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
return Collections.singleton(this); return Collections.singleton(this);
} }
private static ResolveReplacer undefinedReplacer = new ResolveReplacer() {
@Override
protected AbstractConfigValue makeReplacement() throws Undefined {
throw new Undefined();
}
};
@Override @Override
AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve { AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve {
List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size()); List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size());
// if you have "foo = ${?foo}bar" then we will // if you have "foo = ${?foo}bar" then we will
// self-referentially look up foo and we need to // self-referentially look up foo and we need to
// get undefined, rather than "bar" // get undefined, rather than "bar"
context.replace(this, undefinedReplacer); context.source().replace(this, ResolveReplacer.cycleResolveReplacer);
try { try {
for (AbstractConfigValue p : pieces) { for (AbstractConfigValue p : pieces) {
// to concat into a string we have to do a full resolve, // to concat into a string we have to do a full resolve,
@ -138,7 +131,7 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
} }
} }
} finally { } finally {
context.unreplace(this); context.source().unreplace(this);
} }
// now need to concat everything // now need to concat everything

View File

@ -12,7 +12,6 @@ import java.util.List;
import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValueType; import com.typesafe.config.ConfigValueType;
import com.typesafe.config.impl.ResolveReplacer.Undefined;
/** /**
* The issue here is that we want to first merge our stack of config files, and * The issue here is that we want to first merge our stack of config files, and
@ -91,7 +90,7 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
// ConfigDelayedMerge with a value that is only // ConfigDelayedMerge with a value that is only
// the remainder of the stack below this one. // the remainder of the stack below this one.
context.replace((AbstractConfigValue) replaceable, context.source().replace((AbstractConfigValue) replaceable,
replaceable.makeReplacer(count + 1)); replaceable.makeReplacer(count + 1));
replaced = true; replaced = true;
} }
@ -101,7 +100,7 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
resolved = context.resolve(v); resolved = context.resolve(v);
} finally { } finally {
if (replaced) if (replaced)
context.unreplace((AbstractConfigValue) replaceable); context.source().unreplace((AbstractConfigValue) replaceable);
} }
if (resolved != null) { if (resolved != null) {
@ -120,20 +119,21 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
public ResolveReplacer makeReplacer(final int skipping) { public ResolveReplacer makeReplacer(final int skipping) {
return new ResolveReplacer() { return new ResolveReplacer() {
@Override @Override
protected AbstractConfigValue makeReplacement() throws Undefined { protected AbstractConfigValue makeReplacement(ResolveContext context)
return ConfigDelayedMerge.makeReplacement(stack, skipping); throws NotPossibleToResolve {
return ConfigDelayedMerge.makeReplacement(context, stack, skipping);
} }
}; };
} }
// static method also used by ConfigDelayedMergeObject // static method also used by ConfigDelayedMergeObject
static AbstractConfigValue makeReplacement(List<AbstractConfigValue> stack, int skipping) static AbstractConfigValue makeReplacement(ResolveContext context,
throws Undefined { List<AbstractConfigValue> stack, int skipping) throws NotPossibleToResolve {
List<AbstractConfigValue> subStack = stack.subList(skipping, stack.size()); List<AbstractConfigValue> subStack = stack.subList(skipping, stack.size());
if (subStack.isEmpty()) { if (subStack.isEmpty()) {
throw new ResolveReplacer.Undefined(); throw new NotPossibleToResolve(context);
} else { } else {
// generate a new merge stack from only the remaining items // generate a new merge stack from only the remaining items
AbstractConfigValue merged = null; AbstractConfigValue merged = null;

View File

@ -75,8 +75,9 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
public ResolveReplacer makeReplacer(final int skipping) { public ResolveReplacer makeReplacer(final int skipping) {
return new ResolveReplacer() { return new ResolveReplacer() {
@Override @Override
protected AbstractConfigValue makeReplacement() throws Undefined { protected AbstractConfigValue makeReplacement(ResolveContext context)
return ConfigDelayedMerge.makeReplacement(stack, skipping); throws NotPossibleToResolve {
return ConfigDelayedMerge.makeReplacement(context, stack, skipping);
} }
}; };
} }

View File

@ -103,14 +103,33 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
return Collections.singleton(this); return Collections.singleton(this);
} }
// ConfigReference should be a firewall against NotPossibleToResolve going
// further up the stack; it should convert everything to ConfigException.
// This way it's impossible for NotPossibleToResolve to "escape" since
// any failure to resolve has to start with a ConfigReference.
@Override @Override
AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve { AbstractConfigValue resolveSubstitutions(ResolveContext context) {
AbstractConfigValue v = context.source().lookupSubst(context, this, expr, prefixLength); context.source().replace(this, ResolveReplacer.cycleResolveReplacer);
try {
AbstractConfigValue v;
try {
v = context.source().lookupSubst(context, expr, prefixLength);
} catch (NotPossibleToResolve e) {
if (expr.optional())
v = null;
else
throw new ConfigException.UnresolvedSubstitution(origin(), expr
+ " was part of a cycle of substitutions involving " + e.traceString(),
e);
}
if (v == null && !expr.optional()) { if (v == null && !expr.optional()) {
throw new ConfigException.UnresolvedSubstitution(origin(), expr.toString()); throw new ConfigException.UnresolvedSubstitution(origin(), expr.toString());
}
return v;
} finally {
context.source().unreplace(this);
} }
return v;
} }
@Override @Override

View File

@ -1,15 +1,11 @@
package com.typesafe.config.impl; package com.typesafe.config.impl;
import java.util.Collections; import java.util.List;
import java.util.LinkedList; import java.util.ArrayList;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.concurrent.Callable;
import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigResolveOptions; import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve; import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
import com.typesafe.config.impl.AbstractConfigValue.SelfReferential;
final class ResolveContext { final class ResolveContext {
// this is unfortunately mutable so should only be shared among // this is unfortunately mutable so should only be shared among
@ -20,12 +16,6 @@ final class ResolveContext {
// ResolveContext in the same traversal. // ResolveContext in the same traversal.
final private ResolveMemos memos; final private ResolveMemos memos;
// Resolves that we have already begun (for cycle detection).
// SubstitutionResolver separately memoizes completed resolves.
// this set is unfortunately mutable and the user of ResolveContext
// has to be sure it's only shared between ResolveContext that
// are in the same traversal.
final private LinkedList<Set<MemoKey>> traversedStack;
final private ConfigResolveOptions options; final private ConfigResolveOptions options;
// the current path restriction, used to ensure lazy // the current path restriction, used to ensure lazy
// resolution and avoid gratuitous cycles. without this, // resolution and avoid gratuitous cycles. without this,
@ -34,83 +24,25 @@ final class ResolveContext {
// CAN BE NULL for a full resolve. // CAN BE NULL for a full resolve.
final private Path restrictToChild; final private Path restrictToChild;
ResolveContext(ResolveSource source, ResolveMemos memos, // another mutable unfortunate. This is
LinkedList<Set<MemoKey>> traversedStack, ConfigResolveOptions options, // used to make nice error messages when
Path restrictToChild) { // resolution fails.
final private List<SubstitutionExpression> expressionTrace;
ResolveContext(ResolveSource source, ResolveMemos memos, ConfigResolveOptions options,
Path restrictToChild, List<SubstitutionExpression> expressionTrace) {
this.source = source; this.source = source;
this.memos = memos; this.memos = memos;
this.traversedStack = traversedStack;
this.options = options; this.options = options;
this.restrictToChild = restrictToChild; this.restrictToChild = restrictToChild;
this.expressionTrace = expressionTrace;
} }
ResolveContext(AbstractConfigObject root, ConfigResolveOptions options, Path restrictToChild) { ResolveContext(AbstractConfigObject root, ConfigResolveOptions options, Path restrictToChild) {
// LinkedHashSet keeps the traversal order which is at least useful // LinkedHashSet keeps the traversal order which is at least useful
// in error messages if nothing else // in error messages if nothing else
this(new ResolveSource(root), new ResolveMemos(), new LinkedList<Set<MemoKey>>( this(new ResolveSource(root), new ResolveMemos(), options, restrictToChild,
Collections.singletonList(new LinkedHashSet<MemoKey>())), options, restrictToChild); new ArrayList<SubstitutionExpression>());
}
private void traverse(ConfigReference value, SubstitutionExpression via)
throws SelfReferential {
Set<MemoKey> traversed = traversedStack.peekFirst();
MemoKey key = new MemoKey(value, restrictToChild);
if (traversed.contains(key)) {
throw new SelfReferential(value.origin(), via.path().render());
}
traversed.add(key);
}
private void untraverse(ConfigReference value) {
Set<MemoKey> traversed = traversedStack.peekFirst();
MemoKey key = new MemoKey(value, restrictToChild);
if (!traversed.remove(key))
throw new ConfigException.BugOrBroken(
"untraverse() did not find the untraversed substitution " + value);
}
// this just exists to fix the "throws Exception" on Callable
interface Resolver extends Callable<AbstractConfigValue> {
@Override
AbstractConfigValue call() throws NotPossibleToResolve;
}
AbstractConfigValue traversing(ConfigReference value, SubstitutionExpression subst,
Resolver callable) throws NotPossibleToResolve {
try {
traverse(value, subst);
} catch (SelfReferential e) {
if (subst.optional())
return null;
else
throw e;
}
try {
return callable.call();
} finally {
untraverse(value);
}
}
void replace(AbstractConfigValue value, ResolveReplacer replacer) {
source.replace(value, replacer);
// we have to reset the cycle detection because with the
// replacement, a cycle may not exist anymore.
traversedStack.addFirst(new LinkedHashSet<MemoKey>());
}
void unreplace(AbstractConfigValue value) {
source.unreplace(value);
Set<MemoKey> oldTraversed = traversedStack.removeFirst();
if (!oldTraversed.isEmpty())
throw new ConfigException.BugOrBroken(
"unreplace() with stuff still in the traverse set: " + oldTraversed);
} }
ResolveSource source() { ResolveSource source() {
@ -133,15 +65,34 @@ final class ResolveContext {
if (restrictTo == restrictToChild) if (restrictTo == restrictToChild)
return this; return this;
else else
return new ResolveContext(source, memos, traversedStack, options, restrictTo); return new ResolveContext(source, memos, options, restrictTo, expressionTrace);
} }
ResolveContext unrestricted() { ResolveContext unrestricted() {
return restrict(null); return restrict(null);
} }
AbstractConfigValue resolve(AbstractConfigValue original) void trace(SubstitutionExpression expr) {
throws NotPossibleToResolve { expressionTrace.add(expr);
}
void untrace() {
expressionTrace.remove(expressionTrace.size() - 1);
}
String traceString() {
String separator = ", ";
StringBuilder sb = new StringBuilder();
for (SubstitutionExpression expr : expressionTrace) {
sb.append(expr.toString());
sb.append(separator);
}
if (sb.length() > 0)
sb.setLength(sb.length() - separator.length());
return sb.toString();
}
AbstractConfigValue resolve(AbstractConfigValue original) throws NotPossibleToResolve {
// a fully-resolved (no restrictToChild) object can satisfy a // a fully-resolved (no restrictToChild) object can satisfy a
// request for a restricted object, so always check that first. // request for a restricted object, so always check that first.
@ -190,20 +141,20 @@ final class ResolveContext {
} }
static AbstractConfigValue resolve(AbstractConfigValue value, AbstractConfigObject root, static AbstractConfigValue resolve(AbstractConfigValue value, AbstractConfigObject root,
ConfigResolveOptions options, Path restrictToChildOrNull) throws NotPossibleToResolve { ConfigResolveOptions options, Path restrictToChildOrNull) {
ResolveContext context = new ResolveContext(root, options, restrictToChildOrNull);
return context.resolve(value);
}
static AbstractConfigValue resolveWithExternalExceptions(AbstractConfigValue value,
AbstractConfigObject root, ConfigResolveOptions options) {
ResolveContext context = new ResolveContext(root, options, null /* restrictToChild */); ResolveContext context = new ResolveContext(root, options, null /* restrictToChild */);
try { try {
return context.resolve(value); return context.resolve(value);
} catch (NotPossibleToResolve e) { } catch (NotPossibleToResolve e) {
throw e.exportException(value.origin(), null); // ConfigReference was supposed to catch NotPossibleToResolve
throw new ConfigException.BugOrBroken(
"NotPossibleToResolve was thrown from an outermost resolve", e);
} }
} }
static AbstractConfigValue resolve(AbstractConfigValue value, AbstractConfigObject root,
ConfigResolveOptions options) {
return resolve(value, root, options, null);
}
} }

View File

@ -1,15 +1,9 @@
package com.typesafe.config.impl; package com.typesafe.config.impl;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
/** Callback that generates a replacement to use for resolving a substitution. */ /** Callback that generates a replacement to use for resolving a substitution. */
abstract class ResolveReplacer { abstract class ResolveReplacer {
static final class Undefined extends Exception {
private static final long serialVersionUID = 1L;
Undefined() {
super("No replacement, substitution will resolve to undefined");
}
}
// this is a "lazy val" in essence (we only want to // this is a "lazy val" in essence (we only want to
// make the replacement one time). Making it volatile // make the replacement one time). Making it volatile
// is good enough for thread safety as long as this // is good enough for thread safety as long as this
@ -17,11 +11,20 @@ abstract class ResolveReplacer {
// twice has no side effects, which it should not... // twice has no side effects, which it should not...
private volatile AbstractConfigValue replacement = null; private volatile AbstractConfigValue replacement = null;
final AbstractConfigValue replace() throws Undefined { final AbstractConfigValue replace(ResolveContext context) throws NotPossibleToResolve {
if (replacement == null) if (replacement == null)
replacement = makeReplacement(); replacement = makeReplacement(context);
return replacement; return replacement;
} }
protected abstract AbstractConfigValue makeReplacement() throws Undefined; protected abstract AbstractConfigValue makeReplacement(ResolveContext context)
throws NotPossibleToResolve;
static final ResolveReplacer cycleResolveReplacer = new ResolveReplacer() {
@Override
protected AbstractConfigValue makeReplacement(ResolveContext context)
throws NotPossibleToResolve {
throw new NotPossibleToResolve(context);
}
};
} }

View File

@ -1,12 +1,10 @@
package com.typesafe.config.impl; package com.typesafe.config.impl;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigException;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve; import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
import com.typesafe.config.impl.ResolveReplacer.Undefined;
/** /**
* This class is the source for values for a substitution like ${foo}. * This class is the source for values for a substitution like ${foo}.
@ -18,83 +16,79 @@ final class ResolveSource {
// traversed node and therefore avoid circular dependencies. // traversed node and therefore avoid circular dependencies.
// We implement it with this somewhat hacky "patch a replacement" // We implement it with this somewhat hacky "patch a replacement"
// mechanism instead of actually transforming the tree. // mechanism instead of actually transforming the tree.
final private Map<AbstractConfigValue, LinkedList<ResolveReplacer>> replacements; final private Map<AbstractConfigValue, ResolveReplacer> replacements;
ResolveSource(AbstractConfigObject root) { ResolveSource(AbstractConfigObject root) {
this.root = root; this.root = root;
this.replacements = new IdentityHashMap<AbstractConfigValue, LinkedList<ResolveReplacer>>(); this.replacements = new IdentityHashMap<AbstractConfigValue, ResolveReplacer>();
} }
static private AbstractConfigValue findInObject(final AbstractConfigObject obj, static private AbstractConfigValue findInObject(AbstractConfigObject obj,
final ResolveContext context, ConfigReference traversed, ResolveContext context, SubstitutionExpression subst)
final SubstitutionExpression subst) throws NotPossibleToResolve { throws NotPossibleToResolve {
return context.traversing(traversed, subst, new ResolveContext.Resolver() { return obj.peekPath(subst.path(), context);
@Override
public AbstractConfigValue call() throws NotPossibleToResolve {
return obj.peekPath(subst.path(), context);
}
});
} }
AbstractConfigValue lookupSubst(final ResolveContext context, ConfigReference traversed, AbstractConfigValue lookupSubst(ResolveContext context, SubstitutionExpression subst,
final SubstitutionExpression subst, int prefixLength) throws NotPossibleToResolve { int prefixLength) throws NotPossibleToResolve {
// First we look up the full path, which means relative to the context.trace(subst);
// included file if we were not a root file try {
AbstractConfigValue result = findInObject(root, context, traversed, subst); // First we look up the full path, which means relative to the
// included file if we were not a root file
AbstractConfigValue result = findInObject(root, context, subst);
if (result == null) { if (result == null) {
// Then we want to check relative to the root file. We don't // Then we want to check relative to the root file. We don't
// want the prefix we were included at to be used when looking // want the prefix we were included at to be used when looking
// up env variables either. // up env variables either.
SubstitutionExpression unprefixed = subst SubstitutionExpression unprefixed = subst.changePath(subst.path().subPath(
.changePath(subst.path().subPath(prefixLength)); prefixLength));
if (result == null && prefixLength > 0) { // replace the debug trace path
result = findInObject(root, context, traversed, unprefixed); context.untrace();
} context.trace(unprefixed);
if (result == null && context.options().getUseSystemEnvironment()) { if (result == null && prefixLength > 0) {
result = findInObject(ConfigImpl.envVariablesAsConfigObject(), context, traversed, result = findInObject(root, context, unprefixed);
unprefixed);
}
}
if (result != null) {
final AbstractConfigValue unresolved = result;
result = context.traversing(traversed, subst, new ResolveContext.Resolver() {
@Override
public AbstractConfigValue call() throws NotPossibleToResolve {
return context.resolve(unresolved);
} }
});
}
return result; if (result == null && context.options().getUseSystemEnvironment()) {
result = findInObject(ConfigImpl.envVariablesAsConfigObject(), context,
unprefixed);
}
}
if (result != null) {
result = context.resolve(result);
}
return result;
} finally {
context.untrace();
}
} }
void replace(AbstractConfigValue value, ResolveReplacer replacer) { void replace(AbstractConfigValue value, ResolveReplacer replacer) {
LinkedList<ResolveReplacer> stack = replacements.get(value); ResolveReplacer old = replacements.put(value, replacer);
if (stack == null) { if (old != null)
stack = new LinkedList<ResolveReplacer>(); throw new ConfigException.BugOrBroken("should not have replaced the same value twice: "
replacements.put(value, stack); + value);
}
stack.addFirst(replacer);
} }
void unreplace(AbstractConfigValue value) { void unreplace(AbstractConfigValue value) {
LinkedList<ResolveReplacer> stack = replacements.get(value); ResolveReplacer replacer = replacements.remove(value);
if (stack == null) if (replacer == null)
throw new ConfigException.BugOrBroken("unreplace() without replace(): " + value); throw new ConfigException.BugOrBroken("unreplace() without replace(): " + value);
stack.removeFirst();
} }
private AbstractConfigValue replacement(AbstractConfigValue value) throws Undefined { private AbstractConfigValue replacement(ResolveContext context, AbstractConfigValue value)
LinkedList<ResolveReplacer> stack = replacements.get(value); throws NotPossibleToResolve {
if (stack == null || stack.isEmpty()) ResolveReplacer replacer = replacements.get(value);
if (replacer == null) {
return value; return value;
else } else {
return stack.peek().replace(); return replacer.replace(context);
}
} }
/** /**
@ -104,13 +98,8 @@ final class ResolveSource {
AbstractConfigValue resolveCheckingReplacement(ResolveContext context, AbstractConfigValue resolveCheckingReplacement(ResolveContext context,
AbstractConfigValue original) throws NotPossibleToResolve { AbstractConfigValue original) throws NotPossibleToResolve {
AbstractConfigValue replacement; AbstractConfigValue replacement;
boolean forceUndefined = false;
try { replacement = replacement(context, original);
replacement = replacement(original);
} catch (Undefined e) {
replacement = original;
forceUndefined = true;
}
if (replacement != original) { if (replacement != original) {
// start over, checking if replacement was memoized // start over, checking if replacement was memoized
@ -118,10 +107,7 @@ final class ResolveSource {
} else { } else {
AbstractConfigValue resolved; AbstractConfigValue resolved;
if (forceUndefined) resolved = original.resolveSubstitutions(context);
resolved = null;
else
resolved = original.resolveSubstitutions(context);
return resolved; return resolved;
} }

View File

@ -22,7 +22,6 @@ import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions; import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType; import com.typesafe.config.ConfigValueType;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
/** /**
* One thing to keep in mind in the future: as Collection-like APIs are added * One thing to keep in mind in the future: as Collection-like APIs are added
@ -57,8 +56,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
@Override @Override
public SimpleConfig resolve(ConfigResolveOptions options) { public SimpleConfig resolve(ConfigResolveOptions options) {
AbstractConfigValue resolved = ResolveContext.resolveWithExternalExceptions(object, AbstractConfigValue resolved = ResolveContext.resolve(object, object, options);
object, options);
if (resolved == object) if (resolved == object)
return this; return this;
@ -72,9 +70,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
Path path = Path.newPath(pathExpression); Path path = Path.newPath(pathExpression);
ConfigValue peeked; ConfigValue peeked;
try { try {
peeked = object.peekPath(path, null); peeked = object.peekPath(path);
} catch (NotPossibleToResolve e) {
throw e.exportException(origin(), pathExpression);
} catch (ConfigException.NotResolved e) { } catch (ConfigException.NotResolved e) {
throw ConfigImpl.improveNotResolved(pathExpression, e); throw ConfigImpl.improveNotResolved(pathExpression, e);
} }
@ -669,7 +665,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
} }
private AbstractConfigValue peekPath(Path path) { private AbstractConfigValue peekPath(Path path) {
return root().peekPathWithExternalExceptions(path); return root().peekPath(path);
} }
private static void addProblem(List<ConfigException.ValidationProblem> accumulator, Path path, private static void addProblem(List<ConfigException.ValidationProblem> accumulator, Path path,

View File

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

View File

@ -15,20 +15,20 @@ class ConfigSubstitutionTest extends TestUtils {
private def resolveWithoutFallbacks(v: AbstractConfigObject) = { private def resolveWithoutFallbacks(v: AbstractConfigObject) = {
val options = ConfigResolveOptions.noSystem() val options = ConfigResolveOptions.noSystem()
ResolveContext.resolveWithExternalExceptions(v, v, options).asInstanceOf[AbstractConfigObject].toConfig ResolveContext.resolve(v, v, options).asInstanceOf[AbstractConfigObject].toConfig
} }
private def resolveWithoutFallbacks(s: ConfigSubstitution, root: AbstractConfigObject) = { private def resolveWithoutFallbacks(s: ConfigSubstitution, root: AbstractConfigObject) = {
val options = ConfigResolveOptions.noSystem() val options = ConfigResolveOptions.noSystem()
ResolveContext.resolveWithExternalExceptions(s, root, options) ResolveContext.resolve(s, root, options)
} }
private def resolve(v: AbstractConfigObject) = { private def resolve(v: AbstractConfigObject) = {
val options = ConfigResolveOptions.defaults() val options = ConfigResolveOptions.defaults()
ResolveContext.resolveWithExternalExceptions(v, v, options).asInstanceOf[AbstractConfigObject].toConfig ResolveContext.resolve(v, v, options).asInstanceOf[AbstractConfigObject].toConfig
} }
private def resolve(s: ConfigSubstitution, root: AbstractConfigObject) = { private def resolve(s: ConfigSubstitution, root: AbstractConfigObject) = {
val options = ConfigResolveOptions.defaults() val options = ConfigResolveOptions.defaults()
ResolveContext.resolveWithExternalExceptions(s, root, options) ResolveContext.resolve(s, root, options)
} }
private val simpleObject = { private val simpleObject = {
@ -97,10 +97,12 @@ class ConfigSubstitutionTest extends TestUtils {
@Test @Test
def resolveMissingThrows() { def resolveMissingThrows() {
intercept[ConfigException.UnresolvedSubstitution] { val e = intercept[ConfigException.UnresolvedSubstitution] {
val s = subst("bar.missing") val s = subst("bar.missing")
val v = resolveWithoutFallbacks(s, simpleObject) val v = resolveWithoutFallbacks(s, simpleObject)
} }
assertTrue("wrong exception: " + e.getMessage,
!e.getMessage.contains("cycle"))
} }
@Test @Test
@ -218,10 +220,11 @@ class ConfigSubstitutionTest extends TestUtils {
@Test @Test
def throwOnCycles() { def throwOnCycles() {
val s = subst("foo") val s = subst("foo")
val e = intercept[ConfigException.BadValue] { val e = intercept[ConfigException.UnresolvedSubstitution] {
val v = resolveWithoutFallbacks(s, substCycleObject) val v = resolveWithoutFallbacks(s, substCycleObject)
} }
assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle")) assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle"))
assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("${foo}, ${bar}, ${a.b.c}, ${foo}"))
} }
@Test @Test
@ -229,7 +232,7 @@ class ConfigSubstitutionTest extends TestUtils {
// we look up ${?foo}, but the cycle has hard // we look up ${?foo}, but the cycle has hard
// non-optional links in it so still has to throw. // non-optional links in it so still has to throw.
val s = subst("foo", optional = true) val s = subst("foo", optional = true)
val e = intercept[ConfigException.BadValue] { val e = intercept[ConfigException.UnresolvedSubstitution] {
val v = resolveWithoutFallbacks(s, substCycleObject) val v = resolveWithoutFallbacks(s, substCycleObject)
} }
assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle")) assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle"))
@ -256,7 +259,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test @Test
def throwOnTwoKeyCycle() { def throwOnTwoKeyCycle() {
val obj = parseObject("""a:${b},b:${a}""") val obj = parseObject("""a:${b},b:${a}""")
val e = intercept[ConfigException.BadValue] { val e = intercept[ConfigException.UnresolvedSubstitution] {
resolve(obj) resolve(obj)
} }
assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle")) assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle"))
@ -265,7 +268,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test @Test
def throwOnFourKeyCycle() { def throwOnFourKeyCycle() {
val obj = parseObject("""a:${b},b:${c},c:${d},d:${a}""") val obj = parseObject("""a:${b},b:${c},c:${d},d:${a}""")
val e = intercept[ConfigException.BadValue] { val e = intercept[ConfigException.UnresolvedSubstitution] {
resolve(obj) resolve(obj)
} }
assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle")) assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle"))
@ -1023,7 +1026,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test @Test
def substSelfReferenceUndefined() { def substSelfReferenceUndefined() {
val obj = parseObject("""a=${a}""") val obj = parseObject("""a=${a}""")
val e = intercept[ConfigException.BadValue] { val e = intercept[ConfigException.UnresolvedSubstitution] {
resolve(obj) resolve(obj)
} }
assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("cycle")) assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("cycle"))
@ -1039,15 +1042,19 @@ class ConfigSubstitutionTest extends TestUtils {
@Test @Test
def substSelfReferenceIndirect() { def substSelfReferenceIndirect() {
val obj = parseObject("""a=1, b=${a}, a=${b}""") val obj = parseObject("""a=1, b=${a}, a=${b}""")
val resolved = resolve(obj) val e = intercept[ConfigException.UnresolvedSubstitution] {
assertEquals(1, resolved.getInt("a")) resolve(obj)
}
assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("cycle"))
} }
@Test @Test
def substSelfReferenceDoubleIndirect() { def substSelfReferenceDoubleIndirect() {
val obj = parseObject("""a=1, b=${c}, c=${a}, a=${b}""") val obj = parseObject("""a=1, b=${c}, c=${a}, a=${b}""")
val resolved = resolve(obj) val e = intercept[ConfigException.UnresolvedSubstitution] {
assertEquals(1, resolved.getInt("a")) resolve(obj)
}
assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("cycle"))
} }
@Test @Test
@ -1201,7 +1208,7 @@ class ConfigSubstitutionTest extends TestUtils {
@Test @Test
def substInChildFieldNotASelfReference3() { def substInChildFieldNotASelfReference3() {
// checking that having bar.foo earlier in the merge // checking that having bar.foo earlier in the merge
// stack doesn't break the behavior // stack doesn't break the behavior.
val obj = parseObject(""" val obj = parseObject("""
bar : { foo : 43 } bar : { foo : 43 }
bar : { foo : 42, bar : { foo : 42,

View File

@ -20,11 +20,11 @@ import com.typesafe.config.ConfigMergeable
class ConfigTest extends TestUtils { class ConfigTest extends TestUtils {
private def resolveNoSystem(v: AbstractConfigValue, root: AbstractConfigObject) = { private def resolveNoSystem(v: AbstractConfigValue, root: AbstractConfigObject) = {
ResolveContext.resolveWithExternalExceptions(v, root, ConfigResolveOptions.noSystem()) ResolveContext.resolve(v, root, ConfigResolveOptions.noSystem())
} }
private def resolveNoSystem(v: SimpleConfig, root: SimpleConfig) = { private def resolveNoSystem(v: SimpleConfig, root: SimpleConfig) = {
ResolveContext.resolveWithExternalExceptions(v.root, root.root, ResolveContext.resolve(v.root, root.root,
ConfigResolveOptions.noSystem()).asInstanceOf[AbstractConfigObject].toConfig ConfigResolveOptions.noSystem()).asInstanceOf[AbstractConfigObject].toConfig
} }
@ -346,10 +346,10 @@ class ConfigTest extends TestUtils {
// the point here is that we should not try to evaluate a substitution // the point here is that we should not try to evaluate a substitution
// that's been overridden, and thus not end up with a cycle as long // that's been overridden, and thus not end up with a cycle as long
// as we override the problematic link in the cycle. // as we override the problematic link in the cycle.
val e = intercept[ConfigException.BadValue] { val e = intercept[ConfigException.UnresolvedSubstitution] {
val v = resolveNoSystem(subst("foo"), cycleObject) val v = resolveNoSystem(subst("foo"), cycleObject)
} }
assertTrue(e.getMessage().contains("cycle")) assertTrue("wrong exception: " + e.getMessage, e.getMessage().contains("cycle"))
val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : 57 } } } """) val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : 57 } } } """)
val merged = mergeUnresolved(fixUpCycle, cycleObject) val merged = mergeUnresolved(fixUpCycle, cycleObject)
@ -362,10 +362,10 @@ class ConfigTest extends TestUtils {
// the point here is that if our eventual value will be an object, then // the point here is that if our eventual value will be an object, then
// we have to evaluate the substitution to see if it's an object to merge, // we have to evaluate the substitution to see if it's an object to merge,
// so we don't avoid the cycle. // so we don't avoid the cycle.
val e = intercept[ConfigException.BadValue] { val e = intercept[ConfigException.UnresolvedSubstitution] {
val v = resolveNoSystem(subst("foo"), cycleObject) val v = resolveNoSystem(subst("foo"), cycleObject)
} }
assertTrue(e.getMessage().contains("cycle")) assertTrue("wrong exception: " + e.getMessage, e.getMessage().contains("cycle"))
val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : { "q" : "u" } } } } """) val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : { "q" : "u" } } } } """)
val merged = mergeUnresolved(fixUpCycle, cycleObject) val merged = mergeUnresolved(fixUpCycle, cycleObject)

View File

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