From 006777c0629882a3e4c06bc211aadf792735d274 Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Sat, 31 Mar 2012 22:09:05 -0400 Subject: [PATCH] Split ConfigSubstitution into ConfigConcatenation and ConfigReference This makes the code a good bit simpler to reason about, the old ConfigSubstitution really mixed two things into one class. Unfortunately old ConfigSubstitution is hanging around as a compat shim so we can deserialize stuff from the old version of the library. The old version will not be able to deserialize unresolved configs from this new version. In retrospect it might have been better to forbid serialization of unresolved configs and only support serializing resolved configs. --- .../config/impl/ConfigConcatenation.java | 228 ++++++++++++++++ .../typesafe/config/impl/ConfigReference.java | 165 ++++++++++++ .../config/impl/ConfigSubstitution.java | 255 +++++------------- .../java/com/typesafe/config/impl/Parser.java | 11 +- .../typesafe/config/impl/ResolveContext.java | 6 +- .../typesafe/config/impl/ResolveSource.java | 4 +- .../typesafe/config/impl/ConfParserTest.scala | 6 +- .../config/impl/ConfigSubstitutionTest.scala | 79 +++++- .../config/impl/ConfigValueTest.scala | 119 ++++++++ .../com/typesafe/config/impl/TestUtils.scala | 13 +- 10 files changed, 688 insertions(+), 198 deletions(-) create mode 100644 config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java create mode 100644 config/src/main/java/com/typesafe/config/impl/ConfigReference.java 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)