Rewrite substitution resolver, use explicit immutable ResolveSource

The immediate motivation here was to fix #177, which this does,
but in this commit a couple of existing test cases are broken
in a way which seems to relate to order of resolution and resolve
memoization. So we need to layer on to this commit better solutions
for caching and cycle detection to get rid of yet more mutable state.

The previous setup used a side-effect-based lookup table of "replacement"
values to conceptually modify the tree without actually modifying it.
Unfortunately that setup was hacky and hard to reason about and,
apparently, broken in cases such as #177.

This new setup actually creates a modified tree and passes it
around explicitly instead of inside ResolveContext.

In this commit, ResolveContext still unfortunately has a mutable
cache and a mutable table of "cycle markers." Both of those
in theory could also be replaced by simply modifying the tree.

The main downside to this commit - and to cleaning up the remaining
mutable state - is that we're using Java collections which have to
be copied wholesale for every mutation (they are not persistent
functional data structures). This will have an unknown performance
impact, though in a sane world Config.resolve() is not a bottleneck in
anyone's production app.

Some other details of this commit:

 * resolve concerns removed from peekPath in AbstractConfigObject
   and relocated into ResolveSource
 * recursive resolution removed from lookupSubst and moved to
   ConfigReference
 * new hasDescendant() method used only in debug tracing,
   it is grossly inefficient to ever call this full tree
   traversal
 * new replaceChild() method is inefficient due to Java
   collections but could in theory be made efficient
 * most complexity relates to always knowing the parent of
   a node that we might have to replace, so we can walk
   up replacing it in its ancestor chain

TODO in subsequent commits:

 * fix failing test cases
 * we cannot replaceChild if we are a descendant of ConfigConcatenation,
   but we probably (?) need to be able to; consider / fix this
 * instead of memoizing resolve results in a hash table, just
   continuously modify the ResolveSource to have the most recent
   results
 * instead of using the "cycle markers" table, change the
   ConfigReference to a cycle detector value
This commit is contained in:
Havoc Pennington 2014-07-10 12:58:58 -04:00
parent fdce50fb76
commit 0a20b9ad73
17 changed files with 753 additions and 353 deletions

1
config/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/bin

View File

@ -17,7 +17,7 @@ import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
abstract class AbstractConfigObject extends AbstractConfigValue implements ConfigObject {
abstract class AbstractConfigObject extends AbstractConfigValue implements ConfigObject, Container {
final private SimpleConfig config;
@ -56,7 +56,8 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi
/**
* This looks up the key with no transformation or type conversion of any
* kind, and returns null if the key is not present. The object must be
* resolved; use attemptPeekWithPartialResolve() if it is not.
* resolved along the nodes needed to get the key or
* ConfigException.NotResolved will be thrown.
*
* @param key
* @return the unmodified raw value or null
@ -78,67 +79,34 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi
* key to look up
* @return the value of the key, or null if known not to exist
* @throws ConfigException.NotResolved
* if can't figure out key's value or can't know whether it
* exists
* if can't figure out key's value (or existence) without more
* resolving
*/
protected abstract AbstractConfigValue attemptPeekWithPartialResolve(String key);
abstract AbstractConfigValue attemptPeekWithPartialResolve(String key);
/**
* 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
* if context is not null and resolution fails
* Looks up the path with no transformation or type conversion. Returns null
* if the path is not found; throws ConfigException.NotResolved if we need
* to go through an unresolved node to look up the path.
*/
protected AbstractConfigValue peekPath(Path path, ResolveContext context) throws NotPossibleToResolve {
return peekPath(this, path, context);
protected AbstractConfigValue peekPath(Path path) {
return peekPath(this, path);
}
/**
* Looks up the path. Doesn't do any resolution, will throw if any is
* needed.
*/
AbstractConfigValue peekPath(Path path) {
private static AbstractConfigValue peekPath(AbstractConfigObject self, Path path) {
try {
return peekPath(this, path, null);
} catch (NotPossibleToResolve e) {
throw new ConfigException.BugOrBroken(
"NotPossibleToResolve happened though we had no ResolveContext in peekPath");
}
}
// 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());
// 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,
ResolveContext context) throws NotPossibleToResolve {
try {
if (context != null) {
// walk down through the path resolving only things along that
// path, and then recursively call ourselves with no resolve
// context.
AbstractConfigValue partiallyResolved = context.restrict(path).resolve(self);
if (partiallyResolved instanceof AbstractConfigObject) {
return peekPath((AbstractConfigObject) partiallyResolved, path, null);
} else {
throw new ConfigException.BugOrBroken("resolved object to non-object " + self
+ " to " + partiallyResolved);
}
if (next == null) {
return v;
} 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;
if (v instanceof AbstractConfigObject) {
return peekPath((AbstractConfigObject) v, next);
} else {
if (v instanceof AbstractConfigObject) {
return peekPath((AbstractConfigObject) v, next, null);
} else {
return null;
}
return null;
}
}
} catch (ConfigException.NotResolved e) {
@ -209,7 +177,8 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi
}
@Override
abstract AbstractConfigObject resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve;
abstract AbstractConfigObject resolveSubstitutions(ResolveContext context, ResolveSource source)
throws NotPossibleToResolve;
@Override
abstract AbstractConfigObject relativized(final Path prefix);

View File

@ -68,9 +68,11 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
*
* @param context
* state of the current resolve
* @param source
* where to look up values
* @return a new value if there were changes, or this if no changes
*/
AbstractConfigValue resolveSubstitutions(ResolveContext context)
AbstractConfigValue resolveSubstitutions(ResolveContext context, ResolveSource source)
throws NotPossibleToResolve {
return this;
}
@ -79,6 +81,38 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
return ResolveStatus.RESOLVED;
}
protected static List<AbstractConfigValue> replaceChildInList(List<AbstractConfigValue> list,
AbstractConfigValue child, AbstractConfigValue replacement) {
int i = 0;
while (i < list.size() && list.get(i) != child)
++i;
if (i == list.size())
throw new ConfigException.BugOrBroken("tried to replace " + child + " which is not in " + list);
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>(list);
if (replacement != null)
newStack.set(i, replacement);
else
newStack.remove(i);
if (newStack.isEmpty())
return null;
else
return newStack;
}
protected static boolean hasDescendantInList(List<AbstractConfigValue> list, AbstractConfigValue descendant) {
for (AbstractConfigValue v : list) {
if (v == descendant)
return true;
}
// now the expensive traversal
for (AbstractConfigValue v : list) {
if (v instanceof Container && ((Container) v).hasDescendant(descendant))
return true;
}
return false;
}
/**
* This is used when including one file in another; the included file is
* relativized to the path it's included into in the parent file. The point

View File

@ -22,7 +22,7 @@ import com.typesafe.config.ConfigValueType;
* concatenations of objects, but ConfigDelayedMerge should be used for that
* since a concat of objects really will merge, not concatenate.
*/
final class ConfigConcatenation extends AbstractConfigValue implements Unmergeable {
final class ConfigConcatenation extends AbstractConfigValue implements Unmergeable, Container {
final private List<AbstractConfigValue> pieces;
@ -170,7 +170,7 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
}
@Override
AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve {
AbstractConfigValue resolveSubstitutions(ResolveContext context, ResolveSource source) throws NotPossibleToResolve {
if (ConfigImpl.traceSubstitutionsEnabled()) {
int indent = context.depth() + 2;
ConfigImpl.trace(indent - 1, "concatenation has " + pieces.size() + " pieces:");
@ -181,11 +181,16 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
}
}
// Right now there's no reason to pushParent here because the
// content of ConfigConcatenation should not need to replaceChild,
// but if it did we'd have to do this.
ResolveSource sourceWithParent = source; // .pushParent(this);
List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size());
for (AbstractConfigValue p : pieces) {
// to concat into a string we have to do a full resolve,
// so unrestrict the context
AbstractConfigValue r = context.unrestricted().resolve(p);
AbstractConfigValue r = context.unrestricted().resolve(p, sourceWithParent);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "resolved concat piece to " + r);
if (r == null) {
@ -215,6 +220,20 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
return ResolveStatus.UNRESOLVED;
}
@Override
public ConfigConcatenation replaceChild(AbstractConfigValue child, AbstractConfigValue replacement) {
List<AbstractConfigValue> newPieces = replaceChildInList(pieces, child, replacement);
if (newPieces == null)
return null;
else
return new ConfigConcatenation(origin(), newPieces);
}
@Override
public boolean hasDescendant(AbstractConfigValue descendant) {
return hasDescendantInList(pieces, descendant);
}
// when you graft a substitution into another object,
// you have to prefix it with the location in that object
// where you grafted it; but save prefixLength so

View File

@ -54,20 +54,19 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
}
@Override
AbstractConfigValue resolveSubstitutions(ResolveContext context)
AbstractConfigValue resolveSubstitutions(ResolveContext context, ResolveSource source)
throws NotPossibleToResolve {
return resolveSubstitutions(this, stack, context);
return resolveSubstitutions(this, stack, context, source);
}
// static method also used by ConfigDelayedMergeObject
static AbstractConfigValue resolveSubstitutions(ReplaceableMergeStack replaceable,
List<AbstractConfigValue> stack, ResolveContext context) throws NotPossibleToResolve {
static AbstractConfigValue resolveSubstitutions(ReplaceableMergeStack replaceable, List<AbstractConfigValue> stack,
ResolveContext context, ResolveSource source) throws NotPossibleToResolve {
if (ConfigImpl.traceSubstitutionsEnabled()) {
int indent = context.depth() + 2;
ConfigImpl.trace(indent - 1, "delayed merge stack has " + stack.size() + " items:");
ConfigImpl.trace(context.depth(), "delayed merge stack has " + stack.size() + " items:");
int count = 0;
for (AbstractConfigValue v : stack) {
ConfigImpl.trace(indent, count + ": " + v);
ConfigImpl.trace(context.depth() + 1, count + ": " + v);
count += 1;
}
}
@ -75,91 +74,93 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
// 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. If restrictToChildOrNull
// is non-null, we may remain a delayed merge though.
// is non-null, or resolve options allow partial resolves,
// we may remain a delayed merge though.
int count = 0;
AbstractConfigValue merged = null;
for (AbstractConfigValue v : stack) {
if (v instanceof ReplaceableMergeStack)
throw new ConfigException.BugOrBroken(
"A delayed merge should not contain another one: " + replaceable);
for (AbstractConfigValue end : stack) {
// the end value may or may not be resolved already
boolean replaced = false;
// we only replace if we have a substitution, or
// value-concatenation containing one. The Unmergeable
// here isn't a delayed merge stack since we can't contain
// another stack (see assertion above).
if (v instanceof Unmergeable) {
// If, while resolving 'v' we come back to the same
// merge stack, we only want to look _below_ 'v'
ResolveSource sourceForEnd;
if (end instanceof ReplaceableMergeStack)
throw new ConfigException.BugOrBroken("A delayed merge should not contain another one: " + replaceable);
else if (end instanceof Unmergeable) {
// the remainder could be any kind of value, including another
// ConfigDelayedMerge
AbstractConfigValue remainder = replaceable.makeReplacement(context, count + 1);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "remainder portion: " + remainder);
// If, while resolving 'end' we come back to the same
// merge stack, we only want to look _below_ 'end'
// in the stack. So we arrange to replace the
// ConfigDelayedMerge with a value that is only
// the remainder of the stack below this one.
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth() + 1, "because item " + count
+ " in this stack is unresolved, resolving it can only look at remaining "
+ (stack.size() - count - 1) + " items");
context.source().replace((AbstractConfigValue) replaceable,
replaceable.makeReplacer(count + 1));
replaced = true;
}
ConfigImpl.trace(context.depth(), "building sourceForEnd");
// we resetParents() here because we'll be resolving "end"
// against a root which does NOT contain "end"
sourceForEnd = source.replaceWithinCurrentParent((AbstractConfigValue) replaceable, remainder);
AbstractConfigValue resolved;
try {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth() + 1, "resolving item " + count + " in merge stack of "
+ stack.size());
resolved = context.resolve(v);
} finally {
if (replaced)
context.source().unreplace((AbstractConfigValue) replaceable);
ConfigImpl.trace(context.depth(), " sourceForEnd before reset parents but after replace: "
+ sourceForEnd);
sourceForEnd = sourceForEnd.resetParents();
} else {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl
.trace(context.depth(), "will resolve end against the original source with parent pushed");
sourceForEnd = source.pushParent(replaceable);
}
if (resolved != null) {
if (ConfigImpl.traceSubstitutionsEnabled()) {
ConfigImpl.trace(context.depth(), "sourceForEnd =" + sourceForEnd);
}
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "Resolving highest-priority item in delayed merge " + end
+ " against " + sourceForEnd + " endWasRemoved=" + (source != sourceForEnd));
AbstractConfigValue resolvedEnd = context.resolve(end, sourceForEnd);
if (resolvedEnd != null) {
if (merged == null) {
merged = resolved;
merged = resolvedEnd;
} else {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth() + 1, "merging " + merged + " with fallback " + resolved);
merged = merged.withFallback(resolved);
ConfigImpl.trace(context.depth() + 1, "merging " + merged + " with fallback " + resolvedEnd);
merged = merged.withFallback(resolvedEnd);
}
}
count += 1;
}
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth() + 1, "stack was merged to: " + merged);
count += 1;
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "stack merged, yielding: " + merged);
}
return merged;
}
@Override
public ResolveReplacer makeReplacer(final int skipping) {
return new ResolveReplacer() {
@Override
protected AbstractConfigValue makeReplacement(ResolveContext context)
throws NotPossibleToResolve {
return ConfigDelayedMerge.makeReplacement(context, stack, skipping);
}
@Override
public String toString() {
return "ResolveReplacer(ConfigDelayedMerge substack skipping=" + skipping + ")";
}
};
public AbstractConfigValue makeReplacement(ResolveContext context, int skipping) {
return ConfigDelayedMerge.makeReplacement(context, stack, skipping);
}
// static method also used by ConfigDelayedMergeObject
static AbstractConfigValue makeReplacement(ResolveContext context,
List<AbstractConfigValue> stack, int skipping) throws NotPossibleToResolve {
// static method also used by ConfigDelayedMergeObject; end may be null
static AbstractConfigValue makeReplacement(ResolveContext context, List<AbstractConfigValue> stack, int skipping) {
List<AbstractConfigValue> subStack = stack.subList(skipping, stack.size());
if (subStack.isEmpty()) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "Nothing else in the merge stack, can't resolve");
throw new NotPossibleToResolve(context);
ConfigImpl.trace(context.depth(), "Nothing else in the merge stack, replacing with null");
return null;
} else {
// generate a new merge stack from only the remaining items
AbstractConfigValue merged = null;
@ -178,6 +179,20 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
return ResolveStatus.UNRESOLVED;
}
@Override
public AbstractConfigValue replaceChild(AbstractConfigValue child, AbstractConfigValue replacement) {
List<AbstractConfigValue> newStack = replaceChildInList(stack, child, replacement);
if (newStack == null)
return null;
else
return new ConfigDelayedMerge(origin(), newStack);
}
@Override
public boolean hasDescendant(AbstractConfigValue descendant) {
return hasDescendantInList(stack, descendant);
}
@Override
ConfigDelayedMerge relativized(Path prefix) {
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();

View File

@ -50,9 +50,8 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
}
@Override
AbstractConfigObject resolveSubstitutions(ResolveContext context)
throws NotPossibleToResolve {
AbstractConfigValue merged = ConfigDelayedMerge.resolveSubstitutions(this, stack, context);
AbstractConfigObject resolveSubstitutions(ResolveContext context, ResolveSource source) throws NotPossibleToResolve {
AbstractConfigValue merged = ConfigDelayedMerge.resolveSubstitutions(this, stack, context, source);
if (merged instanceof AbstractConfigObject) {
return (AbstractConfigObject) merged;
} else {
@ -62,19 +61,8 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
}
@Override
public ResolveReplacer makeReplacer(final int skipping) {
return new ResolveReplacer() {
@Override
protected AbstractConfigValue makeReplacement(ResolveContext context)
throws NotPossibleToResolve {
return ConfigDelayedMerge.makeReplacement(context, stack, skipping);
}
@Override
public String toString() {
return "ResolveReplacer(ConfigDelayedMergeObject substack skipping=" + skipping + ")";
}
};
public AbstractConfigValue makeReplacement(ResolveContext context, int skipping) {
return ConfigDelayedMerge.makeReplacement(context, stack, skipping);
}
@Override
@ -82,6 +70,20 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
return ResolveStatus.UNRESOLVED;
}
@Override
public AbstractConfigValue replaceChild(AbstractConfigValue child, AbstractConfigValue replacement) {
List<AbstractConfigValue> newStack = replaceChildInList(stack, child, replacement);
if (newStack == null)
return null;
else
return new ConfigDelayedMergeObject(origin(), newStack);
}
@Override
public boolean hasDescendant(AbstractConfigValue descendant) {
return hasDescendantInList(stack, descendant);
}
@Override
ConfigDelayedMergeObject relativized(Path prefix) {
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();

View File

@ -65,12 +65,28 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
// This way it's impossible for NotPossibleToResolve to "escape" since
// any failure to resolve has to start with a ConfigReference.
@Override
AbstractConfigValue resolveSubstitutions(ResolveContext context) {
context.source().replace(this, ResolveReplacer.cycleResolveReplacer);
AbstractConfigValue resolveSubstitutions(ResolveContext context, ResolveSource source) {
context.addCycleMarker(this);
try {
AbstractConfigValue v;
try {
v = context.source().lookupSubst(context, expr, prefixLength);
ResolveSource.ValueWithPath valueWithPath = source.lookupSubst(context, expr, prefixLength);
if (valueWithPath.value != null) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "recursively resolving " + valueWithPath
+ " which was the resolution of " + expr + " against " + source);
ResolveSource recursiveResolveSource = (new ResolveSource(
(AbstractConfigObject) valueWithPath.pathFromRoot.last(), valueWithPath.pathFromRoot));
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "will recursively resolve against " + recursiveResolveSource);
v = context.resolve(valueWithPath.value, recursiveResolveSource);
} else {
v = null;
}
} catch (NotPossibleToResolve e) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(),
@ -79,8 +95,7 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
v = null;
else
throw new ConfigException.UnresolvedSubstitution(origin(), expr
+ " was part of a cycle of substitutions involving " + e.traceString(),
e);
+ " was part of a cycle of substitutions involving " + e.traceString(), e);
}
if (v == null && !expr.optional()) {
@ -92,7 +107,7 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
return v;
}
} finally {
context.source().unreplace(this);
context.removeCycleMarker(this);
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (C) 2014 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
/**
* An AbstractConfigValue which contains other values. Java has no way to
* express "this has to be an AbstractConfigValue also" other than making
* AbstractConfigValue an interface which would be aggravating. But we can say
* we are a ConfigValue.
*/
interface Container extends com.typesafe.config.ConfigValue {
/**
* Replace a child of this value. CAUTION if replacement is null, delete the
* child, which may also delete the parent, or make the parent into a
* non-container.
*/
AbstractConfigValue replaceChild(AbstractConfigValue child, AbstractConfigValue replacement);
/**
* Super-expensive full traversal to see if descendant is anywhere
* underneath this container.
*/
boolean hasDescendant(AbstractConfigValue descendant);
}

View File

@ -2,17 +2,20 @@ package com.typesafe.config.impl;
/** The key used to memoize already-traversed nodes when resolving substitutions */
final class MemoKey {
MemoKey(AbstractConfigValue value, Path restrictToChildOrNull) {
MemoKey(AbstractConfigValue root, AbstractConfigValue value, Path restrictToChildOrNull) {
this.root = root;
this.value = value;
this.restrictToChildOrNull = restrictToChildOrNull;
}
final private AbstractConfigValue root;
final private AbstractConfigValue value;
final private Path restrictToChildOrNull;
@Override
public final int hashCode() {
int h = System.identityHashCode(value);
h = h + 41 * (41 + root.hashCode());
if (restrictToChildOrNull != null) {
return h + 41 * (41 + restrictToChildOrNull.hashCode());
} else {
@ -26,6 +29,8 @@ final class MemoKey {
MemoKey o = (MemoKey) other;
if (o.value != this.value)
return false;
else if (o.root != this.root)
return false;
else if (o.restrictToChildOrNull == this.restrictToChildOrNull)
return true;
else if (o.restrictToChildOrNull == null || this.restrictToChildOrNull == null)

View File

@ -5,10 +5,10 @@ package com.typesafe.config.impl;
* that replaces itself during substitution resolution in order to implement
* "look backwards only" semantics.
*/
interface ReplaceableMergeStack {
interface ReplaceableMergeStack extends Container {
/**
* Make a replacer for this object, skipping the given number of items in
* the stack.
* Make a replacement for this object skipping the given number of elements
* which are lower in merge priority.
*/
ResolveReplacer makeReplacer(int skipping);
AbstractConfigValue makeReplacement(ResolveContext context, int skipping);
}

View File

@ -1,17 +1,16 @@
package com.typesafe.config.impl;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
final class ResolveContext {
// this is unfortunately mutable so should only be shared among
// ResolveContext in the same traversal.
final private ResolveSource source;
// this is unfortunately mutable so should only be shared among
// ResolveContext in the same traversal.
final private ResolveMemos memos;
@ -24,35 +23,42 @@ final class ResolveContext {
// CAN BE NULL for a full resolve.
final private Path restrictToChild;
// another mutable unfortunate. This is
// used to make nice error messages when
// resolution fails.
final private List<SubstitutionExpression> expressionTrace;
// This is used for tracing and debugging and nice error messages;
// contains every node as we call resolve on it.
final private List<AbstractConfigValue> resolveStack;
// This is used for tracing and debugging.
final private List<AbstractConfigValue> treeStack;
final private Set<AbstractConfigValue> cycleMarkers;
ResolveContext(ResolveSource source, ResolveMemos memos, ConfigResolveOptions options, Path restrictToChild,
List<SubstitutionExpression> expressionTrace, List<AbstractConfigValue> treeStack) {
this.source = source;
ResolveContext(ResolveMemos memos, ConfigResolveOptions options, Path restrictToChild,
List<AbstractConfigValue> resolveStack, Set<AbstractConfigValue> cycleMarkers) {
this.memos = memos;
this.options = options;
this.restrictToChild = restrictToChild;
this.expressionTrace = expressionTrace;
this.treeStack = treeStack;
this.resolveStack = resolveStack;
this.cycleMarkers = cycleMarkers;
}
ResolveContext(AbstractConfigObject root, ConfigResolveOptions options, Path restrictToChild) {
ResolveContext(ConfigResolveOptions options, Path restrictToChild) {
// LinkedHashSet keeps the traversal order which is at least useful
// in error messages if nothing else
this(new ResolveSource(root), new ResolveMemos(), options, restrictToChild,
new ArrayList<SubstitutionExpression>(), new ArrayList<AbstractConfigValue>());
this(new ResolveMemos(), options, restrictToChild, new ArrayList<AbstractConfigValue>(), Collections
.newSetFromMap(new IdentityHashMap<AbstractConfigValue, Boolean>()));
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("ResolveContext at root " + root + " restrict to child " + restrictToChild);
ConfigImpl.trace(depth(), "ResolveContext restrict to child " + restrictToChild);
}
ResolveSource source() {
return source;
void addCycleMarker(AbstractConfigValue value) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "++ Cycle marker " + value + "@" + System.identityHashCode(value));
if (cycleMarkers.contains(value))
throw new ConfigException.BugOrBroken("Added cycle marker twice " + value);
cycleMarkers.add(value);
}
void removeCycleMarker(AbstractConfigValue value) {
cycleMarkers.remove(value);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "-- Cycle marker " + value + "@" + System.identityHashCode(value));
}
ConfigResolveOptions options() {
@ -71,66 +77,64 @@ final class ResolveContext {
if (restrictTo == restrictToChild)
return this;
else
return new ResolveContext(source, memos, options, restrictTo, expressionTrace, treeStack);
return new ResolveContext(memos, options, restrictTo, resolveStack, cycleMarkers);
}
ResolveContext unrestricted() {
return restrict(null);
}
void trace(SubstitutionExpression expr) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "pushing expression " + expr);
expressionTrace.add(expr);
}
void untrace() {
SubstitutionExpression expr = expressionTrace.remove(expressionTrace.size() - 1);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "popped expression " + expr);
}
String traceString() {
String separator = ", ";
StringBuilder sb = new StringBuilder();
for (SubstitutionExpression expr : expressionTrace) {
sb.append(expr.toString());
sb.append(separator);
for (AbstractConfigValue value : resolveStack) {
if (value instanceof ConfigReference) {
sb.append(((ConfigReference) value).expression().toString());
sb.append(separator);
}
}
if (sb.length() > 0)
sb.setLength(sb.length() - separator.length());
return sb.toString();
}
void stack(AbstractConfigValue value) {
treeStack.add(value);
private void pushTrace(AbstractConfigValue value) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "pushing trace " + value);
resolveStack.add(value);
}
void unstack() {
treeStack.remove(treeStack.size() - 1);
private void popTrace() {
AbstractConfigValue old = resolveStack.remove(resolveStack.size() - 1);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "popped trace " + old);
}
int depth() {
return treeStack.size();
if (resolveStack.size() > 30)
throw new ConfigException.BugOrBroken("resolve getting too deep");
return resolveStack.size();
}
AbstractConfigValue resolve(AbstractConfigValue original) throws NotPossibleToResolve {
AbstractConfigValue resolve(AbstractConfigValue original, ResolveSource source) throws NotPossibleToResolve {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "resolving " + original);
ConfigImpl
.trace(depth(), "resolving " + original + " restrictToChild=" + restrictToChild + " in " + source);
AbstractConfigValue resolved;
stack(original);
pushTrace(original);
try {
resolved = realResolve(original);
resolved = realResolve(original, source);
} finally {
unstack();
popTrace();
}
return resolved;
}
private AbstractConfigValue realResolve(AbstractConfigValue original) throws NotPossibleToResolve {
private AbstractConfigValue realResolve(AbstractConfigValue original, ResolveSource source)
throws NotPossibleToResolve {
// 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);
final MemoKey fullKey = new MemoKey(source.root, original, null);
MemoKey restrictedKey = null;
AbstractConfigValue cached = memos.get(fullKey);
@ -139,23 +143,32 @@ final class ResolveContext {
// compute the restrictToChild object so use a more limited
// memo key
if (cached == null && isRestrictedToChild()) {
restrictedKey = new MemoKey(original, restrictToChild());
restrictedKey = new MemoKey(source.root, original, restrictToChild());
cached = memos.get(restrictedKey);
}
if (cached != null) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "using cached resolution " + cached + " for " + original);
ConfigImpl.trace(depth(), "using cached resolution " + cached + " for " + original
+ " restrictToChild " + restrictToChild());
return cached;
} else {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(),
"not found in cache, resolving " + original + "@" + System.identityHashCode(original));
AbstractConfigValue resolved = source.resolveCheckingReplacement(this, original);
if (cycleMarkers.contains(original)) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(),
"Cycle detected, can't resolve; " + original + "@" + System.identityHashCode(original));
throw new NotPossibleToResolve(this);
}
AbstractConfigValue resolved = original.resolveSubstitutions(this, source);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "resolved to " + resolved + " from " + original);
ConfigImpl.trace(depth(), "resolved to " + resolved + "@" + System.identityHashCode(resolved)
+ " from " + original + "@" + System.identityHashCode(resolved));
if (resolved == null || resolved.resolveStatus() == ResolveStatus.RESOLVED) {
// if the resolved object is fully resolved by resolving
@ -196,10 +209,11 @@ final class ResolveContext {
static AbstractConfigValue resolve(AbstractConfigValue value, AbstractConfigObject root,
ConfigResolveOptions options) {
ResolveContext context = new ResolveContext(root, options, null /* restrictToChild */);
ResolveSource source = new ResolveSource(root);
ResolveContext context = new ResolveContext(options, null /* restrictToChild */);
try {
return context.resolve(value);
return context.resolve(value, source);
} catch (NotPossibleToResolve e) {
// ConfigReference was supposed to catch NotPossibleToResolve
throw new ConfigException.BugOrBroken(

View File

@ -1,42 +0,0 @@
package com.typesafe.config.impl;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
/** Callback that generates a replacement to use for resolving a substitution. */
abstract class ResolveReplacer {
// this is a "lazy val" in essence (we only want to
// make the replacement one time). Making it volatile
// is good enough for thread safety as long as this
// cache is only an optimization and making the replacement
// twice has no side effects, which it should not...
private volatile AbstractConfigValue replacement = null;
final AbstractConfigValue replace(ResolveContext context) throws NotPossibleToResolve {
if (replacement == null)
replacement = makeReplacement(context);
return replacement;
}
protected abstract AbstractConfigValue makeReplacement(ResolveContext context)
throws NotPossibleToResolve;
static final ResolveReplacer cycleResolveReplacer = new ResolveReplacer() {
@Override
protected AbstractConfigValue makeReplacement(ResolveContext context)
throws NotPossibleToResolve {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "Cycle detected, can't resolve");
throw new NotPossibleToResolve(context);
}
@Override
public String toString() {
return "ResolveReplacer(cycle detector)";
}
};
@Override
public String toString() {
return getClass().getSimpleName() + "(" + replacement + ")";
}
}

View File

@ -1,7 +1,7 @@
package com.typesafe.config.impl;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.NoSuchElementException;
import com.typesafe.config.ConfigException;
import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
@ -10,137 +10,385 @@ import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve;
* This class is the source for values for a substitution like ${foo}.
*/
final class ResolveSource {
final private AbstractConfigObject root;
// Conceptually, we transform the ResolveSource whenever we traverse
// a substitution or delayed merge stack, in order to remove the
// traversed node and therefore avoid circular dependencies.
// We implement it with this somewhat hacky "patch a replacement"
// mechanism instead of actually transforming the tree.
final private Map<AbstractConfigValue, ResolveReplacer> replacements;
final AbstractConfigObject root;
// This is used for knowing the chain of parents we used to get here.
// null if we should assume we are not a descendant of the root.
// the root itself should be a node in this if non-null.
final Node<Container> pathFromRoot;
ResolveSource(AbstractConfigObject root, Node<Container> pathFromRoot) {
this.root = root;
this.pathFromRoot = pathFromRoot;
}
ResolveSource(AbstractConfigObject root) {
this.root = root;
this.replacements = new IdentityHashMap<AbstractConfigValue, ResolveReplacer>();
this.pathFromRoot = null;
}
static private AbstractConfigValue findInObject(AbstractConfigObject obj,
ResolveContext context, SubstitutionExpression subst)
// if we replace the root with a non-object, use an empty
// object with nothing in it instead.
private AbstractConfigObject rootMustBeObj(Container value) {
if (value instanceof AbstractConfigObject) {
return (AbstractConfigObject) value;
} else {
return SimpleConfigObject.empty();
}
}
// as a side effect, findInObject() 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. ValueWithPath.value can be null but
// the ValueWithPath instance itself should not be.
static private ValueWithPath findInObject(AbstractConfigObject obj, ResolveContext context, Path path)
throws NotPossibleToResolve {
return obj.peekPath(subst.path(), context);
// resolve ONLY portions of the object which are along our path
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("*** finding '" + path + "' in " + obj);
AbstractConfigValue partiallyResolved = context.restrict(path).resolve(obj, new ResolveSource(obj));
if (partiallyResolved instanceof AbstractConfigObject) {
return findInObject((AbstractConfigObject) partiallyResolved, path);
} else {
throw new ConfigException.BugOrBroken("resolved object to non-object " + obj + " to " + partiallyResolved);
}
}
AbstractConfigValue lookupSubst(ResolveContext context, SubstitutionExpression subst,
int prefixLength) throws NotPossibleToResolve {
static private ValueWithPath findInObject(AbstractConfigObject obj, Path path) {
try {
// we'll fail if anything along the path can't
// be looked at without resolving.
return findInObject(obj, path, null);
} catch (ConfigException.NotResolved e) {
throw ConfigImpl.improveNotResolved(path, e);
}
}
static private ValueWithPath findInObject(AbstractConfigObject obj, Path path, Node<Container> parents) {
String key = path.first();
Path next = path.remainder();
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("*** looking up '" + key + "' in " + obj);
AbstractConfigValue v = obj.attemptPeekWithPartialResolve(key);
Node<Container> newParents = parents == null ? new Node<Container>(obj) : parents.prepend(obj);
if (next == null) {
return new ValueWithPath(v, newParents);
} else {
if (v instanceof AbstractConfigObject) {
return findInObject((AbstractConfigObject) v, next, newParents);
} else {
return new ValueWithPath(null, newParents);
}
}
}
ValueWithPath lookupSubst(ResolveContext context, SubstitutionExpression subst,
int prefixLength)
throws NotPossibleToResolve {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "searching for " + subst);
context.trace(subst);
try {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), subst + " - looking up relative to file it occurred in");
// 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 (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), subst + " - looking up relative to file it occurred in");
// First we look up the full path, which means relative to the
// included file if we were not a root file
ValueWithPath result = findInObject(root, context, subst.path());
if (result == null) {
// 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
// up env variables either.
SubstitutionExpression unprefixed = subst.changePath(subst.path().subPath(
prefixLength));
if (result == null)
throw new ConfigException.BugOrBroken("findInObject() returned null");
// replace the debug trace path
context.untrace();
context.trace(unprefixed);
if (result.value == null) {
// 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
// up env variables either.
Path unprefixed = subst.path().subPath(prefixLength);
if (prefixLength > 0) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), unprefixed + " - looking up relative to parent file");
result = findInObject(root, context, unprefixed);
}
if (result == null && context.options().getUseSystemEnvironment()) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), unprefixed + " - looking up in system environment");
result = findInObject(ConfigImpl.envVariablesAsConfigObject(), context,
unprefixed);
}
}
if (result != null) {
if (prefixLength > 0) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "recursively resolving " + result
+ " which was the resolution of " + subst);
result = context.resolve(result);
ConfigImpl.trace(context.depth(), unprefixed + " - looking up relative to parent file");
result = findInObject(root, context, unprefixed);
}
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "resolved to " + result);
if (result.value == null && context.options().getUseSystemEnvironment()) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), unprefixed + " - looking up in system environment");
result = findInObject(ConfigImpl.envVariablesAsConfigObject(), context, unprefixed);
}
}
return result;
} finally {
context.untrace();
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "resolved to " + result);
return result;
}
ResolveSource pushParent(Container parent) {
if (parent == null)
throw new ConfigException.BugOrBroken("can't push null parent");
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("pushing parent " + parent + " ==root " + (parent == root) + " onto " + this);
if (pathFromRoot == null) {
if (parent == root) {
return new ResolveSource(root, new Node<Container>(parent));
} else {
if (ConfigImpl.traceSubstitutionsEnabled()) {
// this hasDescendant check is super-expensive so it's a
// trace message rather than an assertion
if (root.hasDescendant((AbstractConfigValue) parent))
ConfigImpl.trace("***** BUG ***** tried to push parent " + parent
+ " without having a path to it in " + this);
}
// ignore parents if we aren't proceeding from the
// root
return this;
}
} else {
Container parentParent = pathFromRoot.head();
if (ConfigImpl.traceSubstitutionsEnabled()) {
// this hasDescendant check is super-expensive so it's a
// trace message rather than an assertion
if (parentParent != null && !parentParent.hasDescendant((AbstractConfigValue) parent))
ConfigImpl.trace("***** BUG ***** trying to push non-child of " + parentParent + ", non-child was "
+ parent);
}
return new ResolveSource(root, pathFromRoot.prepend(parent));
}
}
void replace(AbstractConfigValue value, ResolveReplacer replacer) {
ResolveSource popParent(Container parent) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("Replacing " + value + "@" + System.identityHashCode(value) + " with " + replacer);
ResolveReplacer old = replacements.put(value, replacer);
if (old != null)
throw new ConfigException.BugOrBroken("should not have replaced the same value twice: "
+ value);
ConfigImpl.trace("popping parent " + parent);
if (parent == null)
throw new ConfigException.BugOrBroken("can't pop null parent");
else if (pathFromRoot == null)
return this;
else if (pathFromRoot.head() != parent)
throw new ConfigException.BugOrBroken("parent was not pushed, can't pop: " + parent);
else
return new ResolveSource(root, pathFromRoot.tail());
}
void unreplace(AbstractConfigValue value) {
ResolveReplacer replacer = replacements.remove(value);
if (replacer == null)
throw new ConfigException.BugOrBroken("unreplace() without replace(): " + value);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("Unreplacing " + value + "@" + System.identityHashCode(value) + " which was replaced by "
+ replacer);
ResolveSource resetParents() {
if (pathFromRoot == null)
return this;
else
return new ResolveSource(root);
}
private AbstractConfigValue replacement(ResolveContext context, AbstractConfigValue value)
throws NotPossibleToResolve {
ResolveReplacer replacer = replacements.get(value);
if (replacer == null) {
// returns null if the replacement results in deleting all the nodes.
private static Node<Container> replace(Node<Container> list, Container old, AbstractConfigValue replacement) {
Container child = list.head();
if (child != old)
throw new ConfigException.BugOrBroken("Can only replace() the top node we're resolving; had " + child
+ " on top and tried to replace " + old + " overall list was " + list);
Container parent = list.tail() == null ? null : list.tail().head();
if (replacement == null || !(replacement instanceof Container)) {
if (parent == null) {
return null;
} else {
/*
* we are deleting the child from the stack of containers
* because it's either going away or not a container
*/
AbstractConfigValue newParent = parent.replaceChild((AbstractConfigValue) old, null);
return replace(list.tail(), parent, newParent);
}
} else {
/* we replaced the container with another container */
if (parent == null) {
return new Node<Container>((Container) replacement);
} else {
AbstractConfigValue newParent = parent.replaceChild((AbstractConfigValue) old, replacement);
Node<Container> newTail = replace(list.tail(), parent, newParent);
if (newTail != null)
return newTail.prepend((Container) replacement);
else
return new Node<Container>((Container) replacement);
}
}
}
ResolveSource replaceCurrentParent(Container old, Container replacement) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("replaceCurrentParent old " + old + "@" + System.identityHashCode(old) + " replacement "
+ replacement + "@" + System.identityHashCode(old) + " in " + this);
if (old == replacement) {
return this;
} else if (pathFromRoot != null) {
Node<Container> newPath = replace(pathFromRoot, old, (AbstractConfigValue) replacement);
if (ConfigImpl.traceSubstitutionsEnabled()) {
ConfigImpl.trace("replaced " + old + " with " + replacement + " in " + this);
ConfigImpl.trace("path was: " + pathFromRoot + " is now " + newPath);
}
// if we end up nuking the root object itself, we replace it with an
// empty root
if (newPath != null)
return new ResolveSource((AbstractConfigObject) newPath.last(), newPath);
else
return new ResolveSource(SimpleConfigObject.empty());
} else {
if (old == root) {
return new ResolveSource(rootMustBeObj(replacement));
} else {
throw new ConfigException.BugOrBroken("attempt to replace root " + root + " with " + replacement);
// return this;
}
}
}
// replacement may be null to delete
ResolveSource replaceWithinCurrentParent(AbstractConfigValue old, AbstractConfigValue replacement) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("replaceWithinCurrentParent old " + old + "@" + System.identityHashCode(old)
+ " replacement " + replacement + "@" + System.identityHashCode(old) + " in " + this);
if (old == replacement) {
return this;
} else if (pathFromRoot != null) {
Container parent = pathFromRoot.head();
AbstractConfigValue newParent = parent.replaceChild(old, replacement);
return replaceCurrentParent(parent, (newParent instanceof Container) ? (Container) newParent : null);
} else {
if (old == root && replacement instanceof Container) {
return new ResolveSource(rootMustBeObj((Container) replacement));
} else {
throw new ConfigException.BugOrBroken("replace in parent not possible " + old + " with " + replacement
+ " in " + this);
// return this;
}
}
}
@Override
public String toString() {
return "ResolveSource(root=" + root + ", pathFromRoot=" + pathFromRoot + ")";
}
// a persistent list
static final class Node<T> implements Iterable<T> {
final T value;
final Node<T> next;
Node(T value, Node<T> next) {
this.value = value;
this.next = next;
}
Node(T value) {
this(value, null);
}
Node<T> prepend(T value) {
return new Node<T>(value, this);
}
T head() {
return value;
} else {
AbstractConfigValue replacement = replacer.replace(context);
if (ConfigImpl.traceSubstitutionsEnabled() && value != replacement) {
ConfigImpl.trace(" when looking up substitutions " + context.traceString() + " replaced " + value
+ " with " + replacement);
}
Node<T> tail() {
return next;
}
T last() {
Node<T> i = this;
while (i.next != null)
i = i.next;
return i.value;
}
Node<T> reverse() {
if (next == null) {
return this;
} else {
Node<T> reversed = new Node<T>(value);
Node<T> i = next;
while (i != null) {
reversed = reversed.prepend(i.value);
i = i.next;
}
return reversed;
}
return replacement;
}
private static class NodeIterator<T> implements Iterator<T> {
Node<T> current;
NodeIterator(Node<T> current) {
this.current = current;
}
@Override
public T next() {
if (current == null) {
throw new NoSuchElementException();
} else {
T result = current.value;
current = current.next;
return result;
}
}
@Override
public boolean hasNext() {
return current != null;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<T> iterator() {
return new NodeIterator<T>(this);
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("[");
Node<T> toAppendValue = this.reverse();
while (toAppendValue != null) {
sb.append(toAppendValue.value.toString());
if (toAppendValue.next != null)
sb.append(" <= ");
toAppendValue = toAppendValue.next;
}
sb.append("]");
return sb.toString();
}
}
/**
* Conceptually, this is key.value().resolveSubstitutions() but using the
* replacement for key.value() if any.
*/
AbstractConfigValue resolveCheckingReplacement(ResolveContext context,
AbstractConfigValue original) throws NotPossibleToResolve {
AbstractConfigValue replacement;
// value is allowed to be null
static final class ValueWithPath {
final AbstractConfigValue value;
final Node<Container> pathFromRoot;
replacement = replacement(context, original);
ValueWithPath(AbstractConfigValue value, Node<Container> pathFromRoot) {
this.value = value;
this.pathFromRoot = pathFromRoot;
}
if (replacement != original) {
// start over, checking if replacement was memoized
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "for resolution, replaced " + original + " with " + replacement);
return context.resolve(replacement);
} else {
AbstractConfigValue resolved;
ValueWithPath(AbstractConfigValue value) {
this(value, null);
}
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(),
"resolving unreplaced " + original + " with trace '" + context.traceString() + "'");
resolved = original.resolveSubstitutions(context);
ValueWithPath addParent(Container parent) {
if (pathFromRoot == null)
return new ValueWithPath(value, new Node<Container>(parent));
else
return new ValueWithPath(value, pathFromRoot.prepend(parent));
}
return resolved;
@Override
public String toString() {
return "ValueWithPath(value=" + value + ", pathFromRoot=" + pathFromRoot + ")";
}
}
}

View File

@ -18,7 +18,7 @@ import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
final class SimpleConfigList extends AbstractConfigValue implements ConfigList, Serializable {
final class SimpleConfigList extends AbstractConfigValue implements ConfigList, Container, Serializable {
private static final long serialVersionUID = 2L;
@ -61,6 +61,23 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
return ResolveStatus.fromBoolean(resolved);
}
@Override
public SimpleConfigList replaceChild(AbstractConfigValue child, AbstractConfigValue replacement) {
List<AbstractConfigValue> newList = replaceChildInList(value, child, replacement);
if (newList == null) {
return null;
} else {
// we use the constructor flavor that will recompute the resolve
// status
return new SimpleConfigList(origin(), newList);
}
}
@Override
public boolean hasDescendant(AbstractConfigValue descendant) {
return hasDescendantInList(value, descendant);
}
private SimpleConfigList modify(NoExceptionsModifier modifier, ResolveStatus newResolveStatus) {
try {
return modifyMayThrow(modifier, newResolveStatus);
@ -105,7 +122,8 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
}
@Override
SimpleConfigList resolveSubstitutions(final ResolveContext context) throws NotPossibleToResolve {
SimpleConfigList resolveSubstitutions(final ResolveContext context, ResolveSource source)
throws NotPossibleToResolve {
if (resolved)
return this;
@ -114,12 +132,13 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
// so nothing to do.
return this;
} else {
final ResolveSource sourceWithParent = source.pushParent(this);
try {
return modifyMayThrow(new Modifier() {
@Override
public AbstractConfigValue modifyChildMayThrow(String key, AbstractConfigValue v)
throws NotPossibleToResolve {
return context.resolve(v);
return context.resolve(v, sourceWithParent);
}
}, ResolveStatus.RESOLVED);

View File

@ -198,6 +198,38 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
return ResolveStatus.fromBoolean(resolved);
}
@Override
public SimpleConfigObject replaceChild(AbstractConfigValue child, AbstractConfigValue replacement) {
HashMap<String, AbstractConfigValue> newChildren = new HashMap<String, AbstractConfigValue>(value);
for (Map.Entry<String, AbstractConfigValue> old : newChildren.entrySet()) {
if (old.getValue() == child) {
if (replacement != null)
old.setValue(replacement);
else
newChildren.remove(old.getKey());
return new SimpleConfigObject(origin(), newChildren, ResolveStatus.fromValues(newChildren.values()),
ignoresFallbacks);
}
}
throw new ConfigException.BugOrBroken("SimpleConfigObject.replaceChild did not find " + child + " in " + this);
}
@Override
public boolean hasDescendant(AbstractConfigValue descendant) {
for (AbstractConfigValue child : value.values()) {
if (child == descendant)
return true;
}
// now do the expensive search
for (AbstractConfigValue child : value.values()) {
if (child instanceof Container && ((Container) child).hasDescendant(descendant))
return true;
}
return false;
}
@Override
protected boolean ignoresFallbacks() {
return ignoresFallbacks;
@ -313,21 +345,25 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
}
@Override
AbstractConfigObject resolveSubstitutions(final ResolveContext context) throws NotPossibleToResolve {
AbstractConfigObject resolveSubstitutions(final ResolveContext context, ResolveSource source)
throws NotPossibleToResolve {
if (resolveStatus() == ResolveStatus.RESOLVED)
return this;
final ResolveSource sourceWithParent = source.pushParent(this);
try {
return modifyMayThrow(new Modifier() {
@Override
public AbstractConfigValue modifyChildMayThrow(String key, AbstractConfigValue v)
throws NotPossibleToResolve {
if (context.isRestrictedToChild()) {
if (key.equals(context.restrictToChild().first())) {
Path remainder = context.restrictToChild().remainder();
if (remainder != null) {
return context.restrict(remainder).resolve(v);
return context.restrict(remainder).resolve(v, sourceWithParent);
} else {
// we don't want to resolve the leaf child.
return v;
@ -338,7 +374,7 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
}
} else {
// no restrictToChild, resolve everything
return context.unrestricted().resolve(v);
return context.unrestricted().resolve(v, sourceWithParent);
}
}

View File

@ -360,13 +360,43 @@ class ConcatenationTest extends TestUtils {
// to get there, see https://github.com/typesafehub/config/issues/160
@Test
def plusEqualsMultipleTimesNestedInPlusEquals() {
System.err.println("==============")
val e = intercept[ConfigException.Parse] {
val conf = parseConfig("""x += { a += 1, a += 2, a += 3 } """).resolve()
assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList)
}
assertTrue(e.getMessage.contains("limitation"))
System.err.println("==============")
}
// from https://github.com/typesafehub/config/issues/177
@Test
def arrayConcatenationInDoubleNestedDelayedMerge() {
val unresolved = parseConfig("""d { x = [] }, c : ${d}, c { x += 1, x += 2 }""")
val conf = unresolved.resolve()
assertEquals(Seq(1, 2), conf.getIntList("c.x").asScala)
}
// from https://github.com/typesafehub/config/issues/177
@Test
def arrayConcatenationAsPartOfDelayedMerge() {
val unresolved = parseConfig(""" c { x: [], x : ${c.x}[1], x : ${c.x}[2] }""")
val conf = unresolved.resolve()
assertEquals(Seq(1, 2), conf.getIntList("c.x").asScala)
}
// from https://github.com/typesafehub/config/issues/177
@Test
def arrayConcatenationInDoubleNestedDelayedMerge2() {
val unresolved = parseConfig("""d { x = [] }, c : ${d}, c { x : ${c.x}[1], x : ${c.x}[2] }""")
val conf = unresolved.resolve()
assertEquals(Seq(1, 2), conf.getIntList("c.x").asScala)
}
// from https://github.com/typesafehub/config/issues/177
@Test
def arrayConcatenationInTripleNestedDelayedMerge() {
val unresolved = parseConfig("""{ r: { d.x=[] }, q: ${r}, q : { d { x = [] }, c : ${q.d}, c { x : ${q.c.x}[1], x : ${q.c.x}[2] } } }""")
val conf = unresolved.resolve()
assertEquals(Seq(1, 2), conf.getIntList("q.c.x").asScala)
}
@Test

View File

@ -207,6 +207,16 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(2, resolved.getInt("b"))
}
@Test
def throwOnIncrediblyTrivialCycle() {
val s = subst("a")
val e = intercept[ConfigException.UnresolvedSubstitution] {
val v = resolveWithoutFallbacks(s, parseObject("a: ${a}"))
}
assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("cycle"))
assertTrue("Wrong exception: " + e.getMessage, e.getMessage().contains("${a}"))
}
private val substCycleObject = {
parseObject("""
{
@ -476,9 +486,9 @@ class ConfigSubstitutionTest extends TestUtils {
val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem5)
assertEquals(2, resolved.getInt("item1.b"))
assertEquals(2, resolved.getInt("item2.b"))
assertEquals(2, resolved.getInt("defaults.a"))
assertEquals("item1.b", 2, resolved.getInt("item1.b"))
assertEquals("item2.b", 2, resolved.getInt("item2.b"))
assertEquals("defaults.a", 2, resolved.getInt("defaults.a"))
}
private val delayedMergeObjectResolveProblem6 = {