diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java new file mode 100644 index 00000000..62841b12 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java @@ -0,0 +1,228 @@ +package com.typesafe.config.impl; + +import java.io.ObjectStreamException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +/** + * A ConfigConcatenation represents a list of values to be concatenated (see the + * spec). It only has to exist if at least one value is an unresolved + * substitution, otherwise we could go ahead and collapse the list into a single + * value. + * + * Right now this is always a list of strings and ${} references, but in the + * future should support a list of ConfigList. We may also support + * 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 { + private static final long serialVersionUID = 1L; + + final private List pieces; + + ConfigConcatenation(ConfigOrigin origin, List pieces) { + super(origin); + this.pieces = pieces; + } + + private ConfigException.NotResolved notResolved() { + return new ConfigException.NotResolved( + "need to Config#resolve(), see the API docs for Config#resolve(); substitution not resolved: " + + this); + } + + @Override + public ConfigValueType valueType() { + throw notResolved(); + } + + @Override + public Object unwrapped() { + throw notResolved(); + } + + @Override + protected ConfigConcatenation newCopy(boolean ignoresFallbacks, ConfigOrigin newOrigin) { + return new ConfigConcatenation(newOrigin, pieces); + } + + @Override + protected boolean ignoresFallbacks() { + // we can never ignore fallbacks because if a child ConfigReference + // is self-referential we have to look lower in the merge stack + // for its value. + return false; + } + + @Override + protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.add(this); + newStack.addAll(fallback.unmergedValues()); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + ((AbstractConfigValue) fallback).ignoresFallbacks()); + } + + protected AbstractConfigValue mergedLater(AbstractConfigValue fallback) { + List newStack = new ArrayList(); + newStack.add(this); + newStack.add(fallback); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + fallback.ignoresFallbacks()); + } + + @Override + protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + return mergedLater(fallback); + } + + @Override + protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) { + // We may need the fallback if we contain a self-referential + // ConfigReference. + // + // we can't easily detect the self-referential case since the cycle + // may involve more than one step, so we have to wait and + // merge later when resolving. + return mergedLater(fallback); + } + + @Override + public Collection unmergedValues() { + return Collections.singleton(this); + } + + private static ResolveReplacer undefinedReplacer = new ResolveReplacer() { + @Override + protected AbstractConfigValue makeReplacement() throws Undefined { + throw new Undefined(); + } + }; + + @Override + AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve { + List resolved = new ArrayList(pieces.size()); + // if you have "foo = ${?foo}bar" then we will + // self-referentially look up foo and we need to + // get undefined, rather than "bar" + context.replace(this, undefinedReplacer); + try { + 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); + if (r == null) { + // it was optional... omit + } else { + switch (r.valueType()) { + case LIST: + case OBJECT: + // cannot substitute lists and objects into strings + // we know p was a ConfigReference since it wasn't + // a ConfigString + String pathString = ((ConfigReference) p).expression().toString(); + throw new ConfigException.WrongType(r.origin(), pathString, "not a list or object", r.valueType().name()); + default: + resolved.add(r); + } + } + } + } finally { + context.unreplace(this); + } + + // now need to concat everything + StringBuilder sb = new StringBuilder(); + for (AbstractConfigValue r : resolved) { + sb.append(r.transformToString()); + } + + return new ConfigString(origin(), sb.toString()); + } + + @Override + ResolveStatus resolveStatus() { + return ResolveStatus.UNRESOLVED; + } + + // 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 + // system property and env variable lookups don't get + // broken. + @Override + ConfigConcatenation relativized(Path prefix) { + List newPieces = new ArrayList(); + for (AbstractConfigValue p : pieces) { + newPieces.add(p.relativized(prefix)); + } + return new ConfigConcatenation(origin(), newPieces); + } + + @SuppressWarnings("deprecation") + @Override + protected boolean canEqual(Object other) { + return other instanceof ConfigConcatenation || other instanceof ConfigSubstitution; + } + + @SuppressWarnings("deprecation") + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof ConfigConcatenation) { + return canEqual(other) && this.pieces.equals(((ConfigConcatenation) other).pieces); + } else if (other instanceof ConfigSubstitution) { + return equals(((ConfigSubstitution) other).delegate()); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + return pieces.hashCode(); + } + + @Override + protected void render(StringBuilder sb, int indent, boolean formatted) { + for (AbstractConfigValue p : pieces) { + p.render(sb, indent, formatted); + } + } + + // This ridiculous hack is because some JDK versions apparently can't + // serialize an array, which is used to implement ArrayList and EmptyList. + // maybe + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6446627 + private Object writeReplace() throws ObjectStreamException { + // switch to LinkedList + return new ConfigConcatenation(origin(), new java.util.LinkedList( + pieces)); + } + + static List valuesFromPieces(ConfigOrigin origin, List pieces) { + List values = new ArrayList(pieces.size()); + for (Object p : pieces) { + if (p instanceof SubstitutionExpression) { + values.add(new ConfigReference(origin, (SubstitutionExpression) p)); + } else if (p instanceof String) { + values.add(new ConfigString(origin, (String) p)); + } else { + throw new ConfigException.BugOrBroken("Unexpected piece " + p); + } + } + + return values; + } +} diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java new file mode 100644 index 00000000..82f976cd --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java @@ -0,0 +1,165 @@ +package com.typesafe.config.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +/** + * ConfigReference replaces ConfigReference (the older class kept for back + * compat) and represents the ${} substitution syntax. It can resolve to any + * kind of value. + */ +final class ConfigReference extends AbstractConfigValue implements Unmergeable { + private static final long serialVersionUID = 1L; + + final private SubstitutionExpression expr; + // the length of any prefixes added with relativized() + final private int prefixLength; + + ConfigReference(ConfigOrigin origin, SubstitutionExpression expr) { + this(origin, expr, 0); + } + + private ConfigReference(ConfigOrigin origin, SubstitutionExpression expr, int prefixLength) { + super(origin); + this.expr = expr; + this.prefixLength = prefixLength; + } + + private ConfigException.NotResolved notResolved() { + return new ConfigException.NotResolved( + "need to Config#resolve(), see the API docs for Config#resolve(); substitution not resolved: " + + this); + } + + @Override + public ConfigValueType valueType() { + throw notResolved(); + } + + @Override + public Object unwrapped() { + throw notResolved(); + } + + @Override + protected ConfigReference newCopy(boolean ignoresFallbacks, ConfigOrigin newOrigin) { + if (ignoresFallbacks) + throw new ConfigException.BugOrBroken("Cannot ignore fallbacks for " + this); + return new ConfigReference(newOrigin, expr, prefixLength); + } + + @Override + protected boolean ignoresFallbacks() { + return false; + } + + @Override + protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.add(this); + newStack.addAll(fallback.unmergedValues()); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + ((AbstractConfigValue) fallback).ignoresFallbacks()); + } + + protected AbstractConfigValue mergedLater(AbstractConfigValue fallback) { + List newStack = new ArrayList(); + newStack.add(this); + newStack.add(fallback); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + fallback.ignoresFallbacks()); + } + + @Override + protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + return mergedLater(fallback); + } + + @Override + protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) { + // We may need the fallback for two reasons: + // 1. if an optional substitution ends up getting deleted + // because it is not defined + // 2. if the substitution is self-referential + // + // we can't easily detect the self-referential case since the cycle + // may involve more than one step, so we have to wait and + // merge later when resolving. + return mergedLater(fallback); + } + + @Override + public Collection unmergedValues() { + return Collections.singleton(this); + } + + @Override + AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve { + AbstractConfigValue v = context.source().lookupSubst(context, this, expr, prefixLength); + + if (v == null && !expr.optional()) { + throw new ConfigException.UnresolvedSubstitution(origin(), expr.toString()); + } + return v; + } + + @Override + ResolveStatus resolveStatus() { + return ResolveStatus.UNRESOLVED; + } + + // 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 + // system property and env variable lookups don't get + // broken. + @Override + ConfigReference relativized(Path prefix) { + SubstitutionExpression newExpr = expr.changePath(expr.path().prepend(prefix)); + return new ConfigReference(origin(), newExpr, prefixLength + prefix.length()); + } + + @SuppressWarnings("deprecation") + @Override + protected boolean canEqual(Object other) { + return other instanceof ConfigReference || other instanceof ConfigSubstitution; + } + + @SuppressWarnings("deprecation") + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof ConfigReference) { + return canEqual(other) && this.expr.equals(((ConfigReference) other).expr); + } else if (other instanceof ConfigSubstitution) { + return equals(((ConfigSubstitution) other).delegate()); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + return expr.hashCode(); + } + + @Override + protected void render(StringBuilder sb, int indent, boolean formatted) { + sb.append(expr.toString()); + } + + SubstitutionExpression expression() { + return expr; + } +} diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java b/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java index 473fa325..e2a8b3b8 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java @@ -3,10 +3,9 @@ */ package com.typesafe.config.impl; +import java.io.IOException; import java.io.ObjectStreamException; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import com.typesafe.config.ConfigException; @@ -14,10 +13,11 @@ import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigValueType; /** - * A ConfigSubstitution represents a value with one or more substitutions in it; - * it can resolve to a value of any type, though if the substitution has more - * than one piece it always resolves to a string via value concatenation. + * ConfigSubstitution is now a shim for back-compat with serialization. i.e. an + * old version may unserialize one of these. We now split into ConfigReference + * and ConfigConcatenation. */ +@Deprecated final class ConfigSubstitution extends AbstractConfigValue implements Unmergeable { @@ -27,224 +27,109 @@ final class ConfigSubstitution extends AbstractConfigValue implements // SubstitutionExpression has to be resolved to values, then if there's more // than one piece everything is stringified and concatenated final private List pieces; - // the length of any prefixes added with relativized() - final private int prefixLength; - // this is just here to avoid breaking serialization; - // it is always false at the moment. + // this is just here to avoid breaking serialization + @SuppressWarnings("unused") + @Deprecated + final private int prefixLength = 0; + + // this is just here to avoid breaking serialization + @SuppressWarnings("unused") + @Deprecated final private boolean ignoresFallbacks = false; - ConfigSubstitution(ConfigOrigin origin, List pieces) { - this(origin, pieces, 0, false); - } + // we chain the ConfigSubstitution back-compat stub to a new value + private transient AbstractConfigValue delegate = null; - private ConfigSubstitution(ConfigOrigin origin, List pieces, - int prefixLength, boolean ignoresFallbacks) { - super(origin); - this.pieces = pieces; - this.prefixLength = prefixLength; + private void createDelegate() { + if (delegate != null) + throw new ConfigException.BugOrBroken("creating delegate twice: " + this); - for (Object p : pieces) { - if (p instanceof Path) - throw new RuntimeException("broken here"); + List values = ConfigConcatenation.valuesFromPieces(origin(), pieces); + + if (values.size() == 1) { + delegate = values.get(0); + } else { + delegate = new ConfigConcatenation(origin(), values); } - if (ignoresFallbacks) - throw new ConfigException.BugOrBroken("ConfigSubstitution may never ignore fallbacks"); + if (!(delegate instanceof Unmergeable)) + throw new ConfigException.BugOrBroken("delegate must be Unmergeable: " + this + + " delegate was: " + delegate); } - private ConfigException.NotResolved notResolved() { - return new ConfigException.NotResolved( - "need to Config#resolve(), see the API docs for Config#resolve(); substitution not resolved: " - + this); + AbstractConfigValue delegate() { + if (delegate == null) + throw new NullPointerException("failed to create delegate " + this); + return delegate; + } + + ConfigSubstitution(ConfigOrigin origin, List pieces) { + super(origin); + this.pieces = pieces; + + createDelegate(); } @Override public ConfigValueType valueType() { - throw notResolved(); + return delegate().valueType(); } @Override public Object unwrapped() { - throw notResolved(); + return delegate().unwrapped(); } @Override - protected ConfigSubstitution newCopy(boolean ignoresFallbacks, ConfigOrigin newOrigin) { - return new ConfigSubstitution(newOrigin, pieces, prefixLength, ignoresFallbacks); + protected AbstractConfigValue newCopy(boolean ignoresFallbacks, ConfigOrigin newOrigin) { + return delegate().newCopy(ignoresFallbacks, newOrigin); } @Override protected boolean ignoresFallbacks() { - return ignoresFallbacks; + return delegate().ignoresFallbacks(); } @Override protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { - if (ignoresFallbacks) - throw new ConfigException.BugOrBroken("should not be reached"); - - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.add(this); - newStack.addAll(fallback.unmergedValues()); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - ((AbstractConfigValue) fallback).ignoresFallbacks()); - } - - protected AbstractConfigValue mergedLater(AbstractConfigValue fallback) { - if (ignoresFallbacks) - throw new ConfigException.BugOrBroken("should not be reached"); - - List newStack = new ArrayList(); - newStack.add(this); - newStack.add(fallback); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - fallback.ignoresFallbacks()); + return delegate().mergedWithTheUnmergeable(fallback); } @Override protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - return mergedLater(fallback); + return delegate().mergedWithObject(fallback); } @Override protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) { - // We may need the fallback for two reasons: - // 1. if an optional substitution ends up getting deleted - // because it is not defined - // 2. if the substitution is self-referential - // - // we can't easily detect the self-referential case since the cycle - // may involve more than one step, so we have to wait and - // merge later when resolving. - return mergedLater(fallback); + return delegate().mergedWithNonObject(fallback); } @Override - public Collection unmergedValues() { - return Collections.singleton(this); - } - - List pieces() { - return pieces; - } - - - - private static ResolveReplacer undefinedReplacer = new ResolveReplacer() { - @Override - protected AbstractConfigValue makeReplacement() throws Undefined { - throw new Undefined(); - } - }; - - private AbstractConfigValue resolveValueConcat(ResolveContext context) - throws NotPossibleToResolve { - // need to concat everything into a string - StringBuilder sb = new StringBuilder(); - for (Object p : pieces) { - if (p instanceof String) { - sb.append((String) p); - } else { - SubstitutionExpression exp = (SubstitutionExpression) p; - - // to concat into a string we have to do a full resolve, - // so unrestrict the context - AbstractConfigValue v = context.source().lookupSubst(context.unrestricted(), this, - exp, prefixLength); - - if (v == null) { - if (exp.optional()) { - // append nothing to StringBuilder - } else { - throw new ConfigException.UnresolvedSubstitution(origin(), exp.toString()); - } - } else { - switch (v.valueType()) { - case LIST: - case OBJECT: - // cannot substitute lists and objects into strings - throw new ConfigException.WrongType(v.origin(), exp.path().render(), - "not a list or object", v.valueType().name()); - default: - sb.append(v.transformToString()); - } - } - } - } - return new ConfigString(origin(), sb.toString()); - } - - private AbstractConfigValue resolveSingleSubst(ResolveContext context) - throws NotPossibleToResolve { - - if (!(pieces.get(0) instanceof SubstitutionExpression)) - throw new ConfigException.BugOrBroken( - "ConfigSubstitution should never contain a single String piece"); - - SubstitutionExpression exp = (SubstitutionExpression) pieces.get(0); - AbstractConfigValue v = context.source().lookupSubst(context, this, exp, - prefixLength); - - if (v == null && !exp.optional()) { - throw new ConfigException.UnresolvedSubstitution(origin(), exp.toString()); - } - return v; + public Collection unmergedValues() { + return ((Unmergeable) delegate()).unmergedValues(); } @Override - AbstractConfigValue resolveSubstitutions(ResolveContext context) - throws NotPossibleToResolve { - AbstractConfigValue resolved; - if (pieces.size() > 1) { - // if you have "foo = ${?foo}bar" then we will - // self-referentially look up foo and we need to - // get undefined, rather than "bar" - context.replace(this, undefinedReplacer); - try { - resolved = resolveValueConcat(context); - } finally { - context.unreplace(this); - } - } else { - resolved = resolveSingleSubst(context); - } - return resolved; + AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve { + return context.resolve(delegate()); } @Override ResolveStatus resolveStatus() { - return ResolveStatus.UNRESOLVED; + return delegate().resolveStatus(); } - // 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 - // system property and env variable lookups don't get - // broken. @Override - ConfigSubstitution relativized(Path prefix) { - List newPieces = new ArrayList(); - for (Object p : pieces) { - if (p instanceof SubstitutionExpression) { - SubstitutionExpression exp = (SubstitutionExpression) p; - - newPieces.add(exp.changePath(exp.path().prepend(prefix))); - } else { - newPieces.add(p); - } - } - return new ConfigSubstitution(origin(), newPieces, prefixLength - + prefix.length(), ignoresFallbacks); + AbstractConfigValue relativized(Path prefix) { + return delegate().relativized(prefix); } @Override protected boolean canEqual(Object other) { - return other instanceof ConfigSubstitution; + return other instanceof ConfigSubstitution || other instanceof ConfigReference + || other instanceof ConfigConcatenation; } @Override @@ -254,25 +139,18 @@ final class ConfigSubstitution extends AbstractConfigValue implements return canEqual(other) && this.pieces.equals(((ConfigSubstitution) other).pieces); } else { - return false; + return delegate().equals(other); } } @Override public int hashCode() { - // note that "origin" is deliberately NOT part of equality - return pieces.hashCode(); + return delegate().hashCode(); } @Override protected void render(StringBuilder sb, int indent, boolean formatted) { - for (Object p : pieces) { - if (p instanceof SubstitutionExpression) { - sb.append(p.toString()); - } else { - sb.append(ConfigImplUtil.renderJsonString((String) p)); - } - } + delegate().render(sb, indent, formatted); } // This ridiculous hack is because some JDK versions apparently can't @@ -281,7 +159,20 @@ final class ConfigSubstitution extends AbstractConfigValue implements // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6446627 private Object writeReplace() throws ObjectStreamException { // switch to LinkedList - return new ConfigSubstitution(origin(), new java.util.LinkedList(pieces), - prefixLength, ignoresFallbacks); + return new ConfigSubstitution(origin(), new java.util.LinkedList(pieces)); } + + // generate the delegate when we deserialize to avoid thread safety + // issues later + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + createDelegate(); + } + + // this is a little cleaner but just causes compat issues probably + // private Object readResolve() throws ObjectStreamException { + // replace ourselves on deserialize + // return delegate(); + // } } diff --git a/config/src/main/java/com/typesafe/config/impl/Parser.java b/config/src/main/java/com/typesafe/config/impl/Parser.java index 1ba85352..a2db2a94 100644 --- a/config/src/main/java/com/typesafe/config/impl/Parser.java +++ b/config/src/main/java/com/typesafe/config/impl/Parser.java @@ -335,10 +335,15 @@ final class Parser { if (minimized.size() == 1 && minimized.get(0) instanceof String) { consolidated = Tokens.newString(firstOrigin, (String) minimized.get(0)); + } else if (minimized.size() == 1 && minimized.get(0) instanceof SubstitutionExpression) { + // a substitution expression ${} + consolidated = Tokens.newValue(new ConfigReference(firstOrigin, + (SubstitutionExpression) minimized.get(0))); } else { - // there's some substitution to do later (post-parse step) - consolidated = Tokens.newValue(new ConfigSubstitution( - firstOrigin, minimized)); + // a value concatenation with a substitution expression in it + List vs = ConfigConcatenation.valuesFromPieces( + firstOrigin, minimized); + consolidated = Tokens.newValue(new ConfigConcatenation(firstOrigin, vs)); } putBack(new TokenWithComments(consolidated, firstValueWithComments.comments)); 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 05a5d29e..4e67ebca 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java @@ -51,7 +51,7 @@ final class ResolveContext { Collections.singletonList(new LinkedHashSet())), options, restrictToChild); } - private void traverse(ConfigSubstitution value, SubstitutionExpression via) + private void traverse(ConfigReference value, SubstitutionExpression via) throws SelfReferential { Set traversed = traversedStack.peekFirst(); @@ -63,7 +63,7 @@ final class ResolveContext { traversed.add(key); } - private void untraverse(ConfigSubstitution value) { + private void untraverse(ConfigReference value) { Set traversed = traversedStack.peekFirst(); MemoKey key = new MemoKey(value, restrictToChild); @@ -78,7 +78,7 @@ final class ResolveContext { AbstractConfigValue call() throws NotPossibleToResolve; } - AbstractConfigValue traversing(ConfigSubstitution value, SubstitutionExpression subst, + AbstractConfigValue traversing(ConfigReference value, SubstitutionExpression subst, Resolver callable) throws NotPossibleToResolve { try { traverse(value, subst); 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 ebba168d..ae1aaeb6 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveSource.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveSource.java @@ -26,7 +26,7 @@ final class ResolveSource { } static private AbstractConfigValue findInObject(final AbstractConfigObject obj, - final ResolveContext context, ConfigSubstitution traversed, + final ResolveContext context, ConfigReference traversed, final SubstitutionExpression subst) throws NotPossibleToResolve { return context.traversing(traversed, subst, new ResolveContext.Resolver() { @Override @@ -36,7 +36,7 @@ final class ResolveSource { }); } - AbstractConfigValue lookupSubst(final ResolveContext context, ConfigSubstitution traversed, + AbstractConfigValue lookupSubst(final ResolveContext context, ConfigReference traversed, final SubstitutionExpression subst, int prefixLength) throws NotPossibleToResolve { // First we look up the full path, which means relative to the // included file if we were not a root file diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala index 8ba53f51..e28476d9 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala @@ -77,10 +77,8 @@ class ConfParserTest extends TestUtils { tree match { case list: ConfigList => list.get(0) match { - case subst: ConfigSubstitution => - subst.pieces().get(0) match { - case exp: SubstitutionExpression => exp.path() - } + case ref: ConfigReference => + ref.expression().path() } } } catch { 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 15dfdfa4..033bbd32 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala @@ -705,7 +705,7 @@ class ConfigSubstitutionTest extends TestUtils { } @Test - def serializeUnresolvedObject() { + def deserializeOldUnresolvedObject() { val expectedSerialization = "" + "aced00057372002b636f6d2e74797065736166652e636f6e6669672e696d706c2e53696d706c6543" + "6f6e6669674f626a65637400000000000000010200035a001069676e6f72657346616c6c6261636b" + @@ -784,6 +784,83 @@ class ConfigSubstitutionTest extends TestUtils { "070000000c0000000c7071007e000c71007e000f7000000000007371007e001a7704000000017371" + "007e001c007371007e001f740008707472546f4172727078787878" + checkSerializableOldFormat(expectedSerialization, substComplexObject) + } + + @Test + def serializeUnresolvedObject() { + val expectedSerialization = "" + + "aced00057372002b636f6d2e74797065736166652e636f6e6669672e696d706c2e53696d706c6543" + + "6f6e6669674f626a65637400000000000000010200035a001069676e6f72657346616c6c6261636b" + + "735a00087265736f6c7665644c000576616c756574000f4c6a6176612f7574696c2f4d61703b7872" + + "002d636f6d2e74797065736166652e636f6e6669672e696d706c2e4162737472616374436f6e6669" + + "674f626a65637400000000000000010200014c0006636f6e6669677400274c636f6d2f7479706573" + + "6166652f636f6e6669672f696d706c2f53696d706c65436f6e6669673b7872002c636f6d2e747970" + + "65736166652e636f6e6669672e696d706c2e4162737472616374436f6e66696756616c7565000000" + + "00000000010200014c00066f726967696e74002d4c636f6d2f74797065736166652f636f6e666967" + + "2f696d706c2f53696d706c65436f6e6669674f726967696e3b78707372002b636f6d2e7479706573" + + "6166652e636f6e6669672e696d706c2e53696d706c65436f6e6669674f726967696e000000000000" + + "000102000649000d656e644c696e654e756d62657249000a6c696e654e756d6265724c000e636f6d" + + "6d656e74734f724e756c6c7400104c6a6176612f7574696c2f4c6973743b4c000b64657363726970" + + "74696f6e7400124c6a6176612f6c616e672f537472696e673b4c000a6f726967696e547970657400" + + "254c636f6d2f74797065736166652f636f6e6669672f696d706c2f4f726967696e547970653b4c00" + + "0975726c4f724e756c6c71007e0009787000000002000000027074000b7465737420737472696e67" + + "7e720023636f6d2e74797065736166652e636f6e6669672e696d706c2e4f726967696e5479706500" + + "000000000000001200007872000e6a6176612e6c616e672e456e756d000000000000000012000078" + + "7074000747454e455249437073720025636f6d2e74797065736166652e636f6e6669672e696d706c" + + "2e53696d706c65436f6e66696700000000000000010200014c00066f626a65637474002f4c636f6d" + + "2f74797065736166652f636f6e6669672f696d706c2f4162737472616374436f6e6669674f626a65" + + "63743b787071007e00060000737200116a6176612e7574696c2e486173684d61700507dac1c31660" + + "d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c77" + + "08000000100000000a7400046f626a4573720028636f6d2e74797065736166652e636f6e6669672e" + + "696d706c2e436f6e6669675265666572656e6365000000000000000102000249000c707265666978" + + "4c656e6774684c0004657870727400314c636f6d2f74797065736166652f636f6e6669672f696d70" + + "6c2f537562737469747574696f6e45787072657373696f6e3b7871007e00047371007e0007000000" + + "08000000087071007e000c71007e000f70000000007372002f636f6d2e74797065736166652e636f" + + "6e6669672e696d706c2e537562737469747574696f6e45787072657373696f6e0000000000000001" + + "0200025a00086f7074696f6e616c4c00047061746874001f4c636f6d2f74797065736166652f636f" + + "6e6669672f696d706c2f506174683b7870007372001d636f6d2e74797065736166652e636f6e6669" + + "672e696d706c2e5061746800000000000000010200024c0005666972737471007e00094c00097265" + + "6d61696e64657271007e001c7870740001617371007e001e740001627371007e001e740001657074" + + "0007666f6f2e62617273720022636f6d2e74797065736166652e636f6e6669672e696d706c2e436f" + + "6e666967496e74000000000000000102000149000576616c756578720025636f6d2e747970657361" + + "66652e636f6e6669672e696d706c2e436f6e6669674e756d62657200000000000000010200014c00" + + "0c6f726967696e616c5465787471007e00097871007e00047371007e000700000009000000097071" + + "007e000c71007e000f707400023337000000257400046f626a427371007e00177371007e00070000" + + "0007000000077071007e000c71007e000f70000000007371007e001b007371007e001e7400016173" + + "71007e001e740001627074000361727273720029636f6d2e74797065736166652e636f6e6669672e" + + "696d706c2e53696d706c65436f6e6669674c69737400000000000000010200025a00087265736f6c" + + "7665644c000576616c756571007e00087871007e00047371007e00070000000a0000000a7071007e" + + "000c71007e000f7000737200146a6176612e7574696c2e4c696e6b65644c6973740c29535d4a6088" + + "2203000078707704000000067371007e00177371007e00070000000a0000000a7071007e000c7100" + + "7e000f70000000007371007e001b007371007e001e740003666f6f707371007e001771007e003a00" + + "0000007371007e001b007371007e001e740001617371007e001e740001627371007e001e74000163" + + "707371007e001771007e003a000000007371007e001b007371007e001e740007666f6f2e62617270" + + "7371007e001771007e003a000000007371007e001b007371007e001e7400046f626a427371007e00" + + "1e74000164707371007e001771007e003a000000007371007e001b007371007e001e7400046f626a" + + "417371007e001e740001627371007e001e740001657371007e001e74000166707371007e00177100" + + "7e003a000000007371007e001b007371007e001e7400046f626a457371007e001e74000166707874" + + "00046f626a417371007e00177371007e000700000006000000067071007e000c71007e000f700000" + + "00007371007e001b007371007e001e7400016170740001617371007e00007371007e000700000005" + + "000000057071007e000c71007e000f707371007e001171007e006700007371007e00143f40000000" + + "00000c77080000001000000001740001627371007e00007371007e00070000000500000005707100" + + "7e000c71007e000f707371007e001171007e006c00007371007e00143f4000000000000c77080000" + + "001000000003740001647371007e00177371007e000700000005000000057071007e000c71007e00" + + "0f70000000007371007e001b007371007e001e740003666f6f70740001657371007e00007371007e" + + "000700000005000000057071007e000c71007e000f707371007e001171007e007700007371007e00" + + "143f4000000000000c77080000001000000001740001667371007e001771007e0072000000007371" + + "007e001b007371007e001e740003666f6f7078740001637371007e002671007e0072740002353700" + + "0000397878740003666f6f7371007e00177371007e000700000003000000037071007e000c71007e" + + "000f70000000007371007e001b007371007e001e74000362617270740008707472546f4172727371" + + "007e00177371007e00070000000b0000000b7071007e000c71007e000f70000000007371007e001b" + + "007371007e001e740003617272707400036261727371007e00177371007e00070000000400000004" + + "7071007e000c71007e000f70000000007371007e001b007371007e001e740001617371007e001e74" + + "0001627371007e001e7400016370740001787371007e00007371007e00070000000c0000000c7071" + + "007e000c71007e000f707371007e001171007e009a00007371007e00143f4000000000000c770800" + + "00001000000001740001797371007e00007371007e00070000000c0000000c7071007e000c71007e" + + "000f707371007e001171007e009f00007371007e00143f4000000000000c77080000001000000001" + + "74000d707472546f507472546f4172727371007e00177371007e00070000000c0000000c7071007e" + + "000c71007e000f70000000007371007e001b007371007e001e740008707472546f41727270787878" checkSerializable(expectedSerialization, substComplexObject) } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala index 5598cd93..ef91970d 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala @@ -237,6 +237,9 @@ class ConfigValueTest extends TestUtils { val sameAsA = subst("foo") val b = subst("bar") + assertTrue("wrong type " + a, a.isInstanceOf[ConfigSubstitution]) + assertTrue("wrong type " + b, b.isInstanceOf[ConfigSubstitution]) + checkEqualObjects(a, a) checkEqualObjects(a, sameAsA) checkNotEqualObjects(a, b) @@ -271,6 +274,122 @@ class ConfigValueTest extends TestUtils { val b = checkSerializable(expectedSerialization, a) } + @Test + def configReferenceEquality() { + val a = subst("foo").delegate() + val sameAsA = subst("foo").delegate() + val b = subst("bar").delegate() + val c = subst("foo", optional = true).delegate() + + assertTrue("wrong type " + a, a.isInstanceOf[ConfigReference]) + assertTrue("wrong type " + b, b.isInstanceOf[ConfigReference]) + assertTrue("wrong type " + c, c.isInstanceOf[ConfigReference]) + + checkEqualObjects(a, a) + checkEqualObjects(a, sameAsA) + checkNotEqualObjects(a, b) + checkNotEqualObjects(a, c) + + } + + @Test + def configReferenceSerializable() { + val expectedSerialization = "" + + "aced000573720028636f6d2e74797065736166652e636f6e6669672e696d706c2e436f6e66696752" + + "65666572656e6365000000000000000102000249000c7072656669784c656e6774684c0004657870" + + "727400314c636f6d2f74797065736166652f636f6e6669672f696d706c2f53756273746974757469" + + "6f6e45787072657373696f6e3b7872002c636f6d2e74797065736166652e636f6e6669672e696d70" + + "6c2e4162737472616374436f6e66696756616c756500000000000000010200014c00066f72696769" + + "6e74002d4c636f6d2f74797065736166652f636f6e6669672f696d706c2f53696d706c65436f6e66" + + "69674f726967696e3b78707372002b636f6d2e74797065736166652e636f6e6669672e696d706c2e" + + "53696d706c65436f6e6669674f726967696e000000000000000102000649000d656e644c696e654e" + + "756d62657249000a6c696e654e756d6265724c000e636f6d6d656e74734f724e756c6c7400104c6a" + + "6176612f7574696c2f4c6973743b4c000b6465736372697074696f6e7400124c6a6176612f6c616e" + + "672f537472696e673b4c000a6f726967696e547970657400254c636f6d2f74797065736166652f63" + + "6f6e6669672f696d706c2f4f726967696e547970653b4c000975726c4f724e756c6c71007e000778" + + "70ffffffffffffffff7074000b66616b65206f726967696e7e720023636f6d2e7479706573616665" + + "2e636f6e6669672e696d706c2e4f726967696e5479706500000000000000001200007872000e6a61" + + "76612e6c616e672e456e756d0000000000000000120000787074000747454e455249437000000000" + + "7372002f636f6d2e74797065736166652e636f6e6669672e696d706c2e537562737469747574696f" + + "6e45787072657373696f6e00000000000000010200025a00086f7074696f6e616c4c000470617468" + + "74001f4c636f6d2f74797065736166652f636f6e6669672f696d706c2f506174683b787000737200" + + "1d636f6d2e74797065736166652e636f6e6669672e696d706c2e5061746800000000000000010200" + + "024c0005666972737471007e00074c000972656d61696e64657271007e00107870740003666f6f70" + + val a = subst("foo").delegate() + assertTrue("wrong type " + a, a.isInstanceOf[ConfigReference]) + val b = checkSerializable(expectedSerialization, a) + assertTrue("wrong type " + b, b.isInstanceOf[ConfigReference]) + } + + @Test + def configConcatenationEquality() { + val a = substInString("foo").delegate() + val sameAsA = substInString("foo").delegate() + val b = substInString("bar").delegate() + val c = substInString("foo", optional = true).delegate() + + assertTrue("wrong type " + a, a.isInstanceOf[ConfigConcatenation]) + assertTrue("wrong type " + b, b.isInstanceOf[ConfigConcatenation]) + assertTrue("wrong type " + c, c.isInstanceOf[ConfigConcatenation]) + + checkEqualObjects(a, a) + checkEqualObjects(a, sameAsA) + checkNotEqualObjects(a, b) + checkNotEqualObjects(a, c) + } + + @Test + def configConcatenationSerializable() { + val expectedSerialization = "" + + "aced00057372002c636f6d2e74797065736166652e636f6e6669672e696d706c2e436f6e66696743" + + "6f6e636174656e6174696f6e00000000000000010200014c00067069656365737400104c6a617661" + + "2f7574696c2f4c6973743b7872002c636f6d2e74797065736166652e636f6e6669672e696d706c2e" + + "4162737472616374436f6e66696756616c756500000000000000010200014c00066f726967696e74" + + "002d4c636f6d2f74797065736166652f636f6e6669672f696d706c2f53696d706c65436f6e666967" + + "4f726967696e3b78707372002b636f6d2e74797065736166652e636f6e6669672e696d706c2e5369" + + "6d706c65436f6e6669674f726967696e000000000000000102000649000d656e644c696e654e756d" + + "62657249000a6c696e654e756d6265724c000e636f6d6d656e74734f724e756c6c71007e00014c00" + + "0b6465736372697074696f6e7400124c6a6176612f6c616e672f537472696e673b4c000a6f726967" + + "696e547970657400254c636f6d2f74797065736166652f636f6e6669672f696d706c2f4f72696769" + + "6e547970653b4c000975726c4f724e756c6c71007e00067870ffffffffffffffff7074000b66616b" + + "65206f726967696e7e720023636f6d2e74797065736166652e636f6e6669672e696d706c2e4f7269" + + "67696e5479706500000000000000001200007872000e6a6176612e6c616e672e456e756d00000000" + + "00000000120000787074000747454e4552494370737200146a6176612e7574696c2e4c696e6b6564" + + "4c6973740c29535d4a608822030000787077040000000373720025636f6d2e74797065736166652e" + + "636f6e6669672e696d706c2e436f6e666967537472696e6700000000000000010200014c00057661" + + "6c756571007e00067871007e000271007e000874000673746172743c73720028636f6d2e74797065" + + "736166652e636f6e6669672e696d706c2e436f6e6669675265666572656e63650000000000000001" + + "02000249000c7072656669784c656e6774684c0004657870727400314c636f6d2f74797065736166" + + "652f636f6e6669672f696d706c2f537562737469747574696f6e45787072657373696f6e3b787100" + + "7e000271007e0008000000007372002f636f6d2e74797065736166652e636f6e6669672e696d706c" + + "2e537562737469747574696f6e45787072657373696f6e00000000000000010200025a00086f7074" + + "696f6e616c4c00047061746874001f4c636f6d2f74797065736166652f636f6e6669672f696d706c" + + "2f506174683b7870007372001d636f6d2e74797065736166652e636f6e6669672e696d706c2e5061" + + "746800000000000000010200024c0005666972737471007e00064c000972656d61696e6465727100" + + "7e00177870740003666f6f707371007e001071007e00087400043e656e6478" + + val a = substInString("foo").delegate() + assertTrue("wrong type " + a, a.isInstanceOf[ConfigConcatenation]) + val b = checkSerializable(expectedSerialization, a) + assertTrue("wrong type " + b, b.isInstanceOf[ConfigConcatenation]) + } + + @Test + def configSubstitutionEqualsItsDelegates() { + val a = subst("foo") + assertTrue("wrong type " + a, a.isInstanceOf[ConfigSubstitution]) + val aD = a.delegate() + assertTrue("wrong type " + aD, aD.isInstanceOf[ConfigReference]) + val b = substInString("bar") + assertTrue("wrong type " + b, b.isInstanceOf[ConfigSubstitution]) + val bD = b.delegate() + assertTrue("wrong type " + bD, bD.isInstanceOf[ConfigConcatenation]) + + checkEqualObjects(a, aD) + checkEqualObjects(b, bD) + } + @Test def configDelayedMergeEquality() { val s1 = subst("foo") diff --git a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala index f0c585f5..8ad84f23 100644 --- a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala +++ b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala @@ -103,7 +103,7 @@ abstract trait TestUtils { copy } - protected def checkSerializationCompat[T: Manifest](expectedHex: String, o: T): Unit = { + protected def checkSerializationCompat[T: Manifest](expectedHex: String, o: T, changedOK: Boolean = false): Unit = { // be sure we can still deserialize the old one val inStream = new ByteArrayInputStream(Hex.decodeHex(expectedHex.toCharArray())) val inObjectStream = new ObjectInputStream(inStream) @@ -146,8 +146,9 @@ abstract trait TestUtils { o, deserialized) assertFalse(failure.isDefined) // should have thrown if we had a failure - assertEquals(o.getClass.getSimpleName + " serialization has changed (though we still deserialized the old serialization)", - expectedHex, hex) + if (!changedOK) + assertEquals(o.getClass.getSimpleName + " serialization has changed (though we still deserialized the old serialization)", + expectedHex, hex) } catch { case e: Throwable => showCorrectResult() @@ -161,6 +162,12 @@ abstract trait TestUtils { t } + protected def checkSerializableOldFormat[T: Manifest](expectedHex: String, o: T): T = { + val t = checkSerializable(o) + checkSerializationCompat(expectedHex, o, changedOK = true) + t + } + protected def checkSerializable[T: Manifest](o: T): T = { checkEqualObjects(o, o)