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 4d74bef6..00990d1d 100644 --- a/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -113,8 +113,8 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi // walk down through the path resolving only things along that // path, // and then recursively call ourselves with no resolver. - AbstractConfigValue partiallyResolved = resolver.resolve(self, - context.restrict(path)); + AbstractConfigValue partiallyResolved = context.restrict(path).resolve(resolver, + self); if (partiallyResolved instanceof AbstractConfigObject) { return peekPath((AbstractConfigObject) partiallyResolved, path, null, null); } else { 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 b97a8ea1..38ef1532 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java @@ -99,7 +99,7 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl AbstractConfigValue resolved; try { - resolved = resolver.resolve(v, context); + resolved = context.resolve(resolver, v); } finally { if (replaced) context.unreplace((AbstractConfigValue) replaceable); 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 05e65b9e..204dbefb 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java @@ -18,6 +18,11 @@ 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; + // Resolves that we have already begun (for cycle detection). // SubstitutionResolver separately memoizes completed resolves. // this set is unfortunately mutable and the user of ResolveContext @@ -33,10 +38,11 @@ final class ResolveContext { // given replacement instead. final private Map> replacements; - ResolveContext(ResolveSource source, LinkedList> traversedStack, - ConfigResolveOptions options, Path restrictToChild, - Map> replacements) { + ResolveContext(ResolveSource source, ResolveMemos memos, + LinkedList> traversedStack, ConfigResolveOptions options, + Path restrictToChild, Map> replacements) { this.source = source; + this.memos = memos; this.traversedStack = traversedStack; this.options = options; this.restrictToChild = restrictToChild; @@ -46,9 +52,9 @@ final class ResolveContext { ResolveContext(AbstractConfigObject root, 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 LinkedList>( - Collections.singletonList(new LinkedHashSet())), - options, restrictToChild, new HashMap>()); + this(new ResolveSource(root), new ResolveMemos(), new LinkedList>( + Collections.singletonList(new LinkedHashSet())), options, restrictToChild, + new HashMap>()); } private void traverse(ConfigSubstitution value, SubstitutionExpression via) @@ -150,10 +156,81 @@ final class ResolveContext { if (restrictTo == restrictToChild) return this; else - return new ResolveContext(source, traversedStack, options, restrictTo, replacements); + return new ResolveContext(source, memos, traversedStack, options, restrictTo, + replacements); } ResolveContext unrestricted() { return restrict(null); } + + AbstractConfigValue resolve(SubstitutionResolver resolver, AbstractConfigValue original) + 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); + MemoKey restrictedKey = null; + + AbstractConfigValue cached = memos.get(fullKey); + + // but if there was no fully-resolved object cached, we'll only + // compute the restrictToChild object so use a more limited + // memo key + if (cached == null && isRestrictedToChild()) { + restrictedKey = new MemoKey(original, restrictToChild()); + cached = memos.get(restrictedKey); + } + + if (cached != null) { + return cached; + } else { + MemoKey key = restrictedKey != null ? restrictedKey : fullKey; + + AbstractConfigValue replacement; + boolean forceUndefined = false; + try { + replacement = replacement(key); + } catch (Undefined e) { + replacement = original; + forceUndefined = true; + } + + if (replacement != original) { + // start over, checking if replacement was memoized + return resolve(resolver, replacement); + } else { + AbstractConfigValue resolved; + + if (forceUndefined) + resolved = null; + else + resolved = original.resolveSubstitutions(resolver, this); + + if (resolved == null || resolved.resolveStatus() == ResolveStatus.RESOLVED) { + // if the resolved object is fully resolved by resolving + // only the restrictToChildOrNull, then it can be cached + // under fullKey since the child we were restricted to + // turned out to be the only unresolved thing. + memos.put(fullKey, resolved); + } else { + // if we have an unresolved object then either we did a + // partial resolve restricted to a certain child, or it's + // a bug. + if (isRestrictedToChild()) { + if (restrictedKey == null) { + throw new ConfigException.BugOrBroken( + "restrictedKey should not be null here"); + } + memos.put(restrictedKey, resolved); + } else { + throw new ConfigException.BugOrBroken( + "resolveSubstitutions() did not give us a resolved object"); + } + } + + return resolved; + } + } + } } diff --git a/config/src/main/java/com/typesafe/config/impl/ResolveMemos.java b/config/src/main/java/com/typesafe/config/impl/ResolveMemos.java new file mode 100644 index 00000000..dc43d727 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/ResolveMemos.java @@ -0,0 +1,27 @@ +package com.typesafe.config.impl; + +import java.util.HashMap; +import java.util.Map; + +/** + * This exists because we have to memoize resolved substitutions as we go + * through the config tree; otherwise we could end up creating multiple copies + * of values or whole trees of values as we follow chains of substitutions. + */ +final class ResolveMemos { + // note that we can resolve things to undefined (represented as Java null, + // rather than ConfigNull) so this map can have null values. + final private Map memos; + + ResolveMemos() { + this.memos = new HashMap(); + } + + AbstractConfigValue get(MemoKey key) { + return memos.get(key); + } + + void put(MemoKey key, AbstractConfigValue value) { + memos.put(key, value); + } +} 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 bab97a43..4b6c301a 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveSource.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveSource.java @@ -54,7 +54,7 @@ final class ResolveSource { result = context.traversing(traversed, subst, new ResolveContext.Resolver() { @Override public AbstractConfigValue call() throws NotPossibleToResolve { - return resolver.resolve(unresolved, context); + return context.resolve(resolver, unresolved); } }); } 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 68a7d927..30f9be0a 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java @@ -118,7 +118,7 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList { @Override public AbstractConfigValue modifyChildMayThrow(String key, AbstractConfigValue v) throws NotPossibleToResolve { - return resolver.resolve(v, context); + return context.resolve(resolver, v); } }, 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 7f6c05b4..a95dc8b6 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -274,7 +274,7 @@ final class SimpleConfigObject extends AbstractConfigObject { if (key.equals(context.restrictToChild().first())) { Path remainder = context.restrictToChild().remainder(); if (remainder != null) { - return resolver.resolve(v, context.restrict(remainder)); + return context.restrict(remainder).resolve(resolver, v); } else { // we don't want to resolve the leaf child. return v; @@ -285,7 +285,7 @@ final class SimpleConfigObject extends AbstractConfigObject { } } else { // no restrictToChild, resolve everything - return resolver.resolve(v, context.unrestricted()); + return context.unrestricted().resolve(resolver, v); } } diff --git a/config/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java b/config/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java index 10f73a21..762288b6 100644 --- a/config/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java +++ b/config/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java @@ -3,96 +3,16 @@ */ package com.typesafe.config.impl; -import java.util.HashMap; -import java.util.Map; - -import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigResolveOptions; import com.typesafe.config.impl.AbstractConfigValue.NotPossibleToResolve; -import com.typesafe.config.impl.ResolveReplacer.Undefined; /** - * This exists because we have to memoize resolved substitutions as we go - * through the config tree; otherwise we could end up creating multiple copies - * of values or whole trees of values as we follow chains of substitutions. + * TODO there is no reason for this class to exist, will remove in upcoming + * commit */ final class SubstitutionResolver { - // note that we can resolve things to undefined (represented as Java null, - // rather than ConfigNull) so this map can have null values. - final private Map memos; - SubstitutionResolver() { - this.memos = new HashMap(); - } - AbstractConfigValue resolve(AbstractConfigValue original, ResolveContext context) - 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); - MemoKey restrictedKey = null; - - AbstractConfigValue cached = memos.get(fullKey); - - // but if there was no fully-resolved object cached, we'll only - // compute the restrictToChild object so use a more limited - // memo key - if (cached == null && context.isRestrictedToChild()) { - restrictedKey = new MemoKey(original, context.restrictToChild()); - cached = memos.get(restrictedKey); - } - - if (cached != null) { - return cached; - } else { - MemoKey key = restrictedKey != null ? restrictedKey : fullKey; - - AbstractConfigValue replacement; - boolean forceUndefined = false; - try { - replacement = context.replacement(key); - } catch (Undefined e) { - replacement = original; - forceUndefined = true; - } - - if (replacement != original) { - // start over, checking if replacement was memoized - return resolve(replacement, context); - } else { - AbstractConfigValue resolved; - - if (forceUndefined) - resolved = null; - else - resolved = original.resolveSubstitutions(this, context); - - if (resolved == null || resolved.resolveStatus() == ResolveStatus.RESOLVED) { - // if the resolved object is fully resolved by resolving - // only the restrictToChildOrNull, then it can be cached - // under fullKey since the child we were restricted to - // turned out to be the only unresolved thing. - memos.put(fullKey, resolved); - } else { - // if we have an unresolved object then either we did a - // partial resolve restricted to a certain child, or it's - // a bug. - if (context.isRestrictedToChild()) { - if (restrictedKey == null) { - throw new ConfigException.BugOrBroken( - "restrictedKey should not be null here"); - } - memos.put(restrictedKey, resolved); - } else { - throw new ConfigException.BugOrBroken( - "resolveSubstitutions() did not give us a resolved object"); - } - } - - return resolved; - } - } } static AbstractConfigValue resolve(AbstractConfigValue value, AbstractConfigObject root, @@ -100,7 +20,7 @@ final class SubstitutionResolver { SubstitutionResolver resolver = new SubstitutionResolver(); ResolveContext context = new ResolveContext(root, options, restrictToChildOrNull); - return resolver.resolve(value, context); + return context.resolve(resolver, value); } static AbstractConfigValue resolveWithExternalExceptions(AbstractConfigValue value, @@ -109,7 +29,7 @@ final class SubstitutionResolver { ResolveContext context = new ResolveContext(root, options, null /* restrictToChild */); try { - return resolver.resolve(value, context); + return context.resolve(resolver, value); } catch (NotPossibleToResolve e) { throw e.exportException(value.origin(), null); }