diff --git a/config/.gitignore b/config/.gitignore new file mode 100644 index 00000000..5e56e040 --- /dev/null +++ b/config/.gitignore @@ -0,0 +1 @@ +/bin diff --git a/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java b/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java index 394931de..bcda80d7 100644 --- a/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -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); diff --git a/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java b/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java index 9b6b1fd9..1eb05c81 100644 --- a/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java +++ b/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java @@ -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 replaceChildInList(List 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 newStack = new ArrayList(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 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 diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java index 8cb15753..e4f031eb 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java @@ -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 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 resolved = new ArrayList(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 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 diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java index 206f18b0..32a8ea65 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java @@ -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 stack, ResolveContext context) throws NotPossibleToResolve { + static AbstractConfigValue resolveSubstitutions(ReplaceableMergeStack replaceable, List 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 stack, int skipping) throws NotPossibleToResolve { - + // static method also used by ConfigDelayedMergeObject; end may be null + static AbstractConfigValue makeReplacement(ResolveContext context, List stack, int skipping) { List 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 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 newStack = new ArrayList(); diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java index 6b11f108..f9afd6d7 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java @@ -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 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 newStack = new ArrayList(); diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java index 204a0aba..963157cb 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java @@ -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); } } diff --git a/config/src/main/java/com/typesafe/config/impl/Container.java b/config/src/main/java/com/typesafe/config/impl/Container.java new file mode 100644 index 00000000..73a40391 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/Container.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2014 Typesafe Inc. + */ +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); +} diff --git a/config/src/main/java/com/typesafe/config/impl/MemoKey.java b/config/src/main/java/com/typesafe/config/impl/MemoKey.java index cd843dad..d948aed0 100644 --- a/config/src/main/java/com/typesafe/config/impl/MemoKey.java +++ b/config/src/main/java/com/typesafe/config/impl/MemoKey.java @@ -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) diff --git a/config/src/main/java/com/typesafe/config/impl/ReplaceableMergeStack.java b/config/src/main/java/com/typesafe/config/impl/ReplaceableMergeStack.java index 7e5dc9d1..8425a5a3 100644 --- a/config/src/main/java/com/typesafe/config/impl/ReplaceableMergeStack.java +++ b/config/src/main/java/com/typesafe/config/impl/ReplaceableMergeStack.java @@ -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); } diff --git a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java index 2f17c0b8..49b1ec22 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java @@ -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 expressionTrace; + // This is used for tracing and debugging and nice error messages; + // contains every node as we call resolve on it. + final private List resolveStack; - // This is used for tracing and debugging. - final private List treeStack; + final private Set cycleMarkers; - ResolveContext(ResolveSource source, ResolveMemos memos, ConfigResolveOptions options, Path restrictToChild, - List expressionTrace, List treeStack) { - this.source = source; + ResolveContext(ResolveMemos memos, ConfigResolveOptions options, Path restrictToChild, + List resolveStack, Set 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(), new ArrayList()); + this(new ResolveMemos(), options, restrictToChild, new ArrayList(), Collections + .newSetFromMap(new IdentityHashMap())); 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( diff --git a/config/src/main/java/com/typesafe/config/impl/ResolveReplacer.java b/config/src/main/java/com/typesafe/config/impl/ResolveReplacer.java deleted file mode 100644 index b3c2549f..00000000 --- a/config/src/main/java/com/typesafe/config/impl/ResolveReplacer.java +++ /dev/null @@ -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 + ")"; - } -} diff --git a/config/src/main/java/com/typesafe/config/impl/ResolveSource.java b/config/src/main/java/com/typesafe/config/impl/ResolveSource.java index 65fefccd..514c38a7 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveSource.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveSource.java @@ -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 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 pathFromRoot; + + ResolveSource(AbstractConfigObject root, Node pathFromRoot) { + this.root = root; + this.pathFromRoot = pathFromRoot; + } ResolveSource(AbstractConfigObject root) { this.root = root; - this.replacements = new IdentityHashMap(); + 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 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 newParents = parents == null ? new Node(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(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 replace(Node 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) replacement); + } else { + AbstractConfigValue newParent = parent.replaceChild((AbstractConfigValue) old, replacement); + Node newTail = replace(list.tail(), parent, newParent); + if (newTail != null) + return newTail.prepend((Container) replacement); + else + return new Node((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 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 implements Iterable { + final T value; + final Node next; + + Node(T value, Node next) { + this.value = value; + this.next = next; + } + + Node(T value) { + this(value, null); + } + + Node prepend(T value) { + return new Node(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 tail() { + return next; + } + + T last() { + Node i = this; + while (i.next != null) + i = i.next; + return i.value; + } + + Node reverse() { + if (next == null) { + return this; + } else { + Node reversed = new Node(value); + Node i = next; + while (i != null) { + reversed = reversed.prepend(i.value); + i = i.next; + } + return reversed; } - return replacement; + } + + private static class NodeIterator implements Iterator { + Node current; + + NodeIterator(Node 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 iterator() { + return new NodeIterator(this); + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("["); + Node 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 pathFromRoot; - replacement = replacement(context, original); + ValueWithPath(AbstractConfigValue value, Node 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(parent)); + else + return new ValueWithPath(value, pathFromRoot.prepend(parent)); + } - return resolved; + @Override + public String toString() { + return "ValueWithPath(value=" + value + ", pathFromRoot=" + pathFromRoot + ")"; } } } diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java index d197cd25..038e0b9e 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java @@ -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 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); diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java index 7667a20f..e652af61 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -198,6 +198,38 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa return ResolveStatus.fromBoolean(resolved); } + @Override + public SimpleConfigObject replaceChild(AbstractConfigValue child, AbstractConfigValue replacement) { + HashMap newChildren = new HashMap(value); + for (Map.Entry 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); } } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala index f59b3251..be30a46e 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala @@ -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 diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala index 2c3b067c..2eff0545 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala @@ -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 = {