mirror of
https://github.com/lightbend/config.git
synced 2025-01-29 05:30:08 +08:00
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:
parent
fdce50fb76
commit
0a20b9ad73
1
config/.gitignore
vendored
Normal file
1
config/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/bin
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>();
|
||||
|
@ -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>();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
25
config/src/main/java/com/typesafe/config/impl/Container.java
Normal file
25
config/src/main/java/com/typesafe/config/impl/Container.java
Normal 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);
|
||||
}
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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 + ")";
|
||||
}
|
||||
}
|
@ -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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user