Support variable substitution in includes

This commit is contained in:
Ryan O'Neill 2015-08-05 15:54:24 -07:00
parent aac122ae01
commit 730ad27628
24 changed files with 95 additions and 260 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.classpath .classpath
.project .project
.cache .cache
.DS_Store
.settings .settings
.idea .idea
.idea_modules .idea_modules

View File

@ -1,59 +0,0 @@
# Submitting pull requests
Pull requests should go via GitHub; there are some nice
[general guidelines for contributing on GitHub](https://guides.github.com/activities/contributing-to-open-source/)
if you haven't done it before.
Unless your fix is trivial, it's a good idea to look for related
discussions and maybe open an issue to discuss, before making a
pull request. Discussion in advance may keep you from wasting time
on an approach that won't work out. However, if you communicate
better in code than in words, starting with a patch is
fine... just be willing to revise it!
Before we can accept pull requests, you will need to agree to the
Typesafe Contributor License Agreement online, using your GitHub
account - it takes 30 seconds. You can do this at
http://www.typesafe.com/contribute/cla
Expect that most PRs will need revision before merge. If people
suggest revisions, you can make them yourself or wait for a
maintainer to do it on their own timeline. The larger the PR, the
more revision will likely be needed.
# Making a release
To make a release you'll need to be a maintainer with GitHub
permissions to push to the master and gh-pages branches, and
Sonatype permissions to publish.
Here are the steps, which should be automated but aren't (PR
welcome!):
1. write release notes in NEWS.md following the format
already in there. commit.
2. create a signed git tag "vX.Y.Z"
3. start sbt; `show version` should confirm that the version was
taken from the tag
4. clean
5. test (double check that release works)
6. doc (double check that docs build, plus build docs
to be copied to gh-pages later)
7. if test or doc fails, delete the tag, fix it, start over.
8. publishSigned
9. make a separate clone of the repo in another directory and
check out the gh-pages branch
10. /bin/rm -rf latest/api on gh-pages checkout
11. copy config/target/api from master checkout to vX.Y.Z in
gh-pages so you have vX.Y.Z/index.html
12. copy config/target/api from master checkout into latest/
so you have latest/api/index.html
13. commit all that to gh-pages, check the diff for sanity
(latest/api should be mostly just changed timestamps)
14. push gh-pages
15. log into sonatype website and go through the usual hoops
(search for com.typesafe, verify the artifacts in it, close,
release)
16. push the "vX.Y.Z" tag
17. announce release, possibly wait for maven central to sync
first

View File

@ -1025,6 +1025,19 @@ Then the `${x}` in "foo.conf", which has been fixed up to
`${a.x}`, would evaluate to `42` rather than to `10`. `${a.x}`, would evaluate to `42` rather than to `10`.
Substitution happens _after_ parsing the whole configuration. Substitution happens _after_ parsing the whole configuration.
Includes inside of lists use the list index as a path segment when
fixing up substitutions.
Example:
{
a : [ { include "foo.conf" } ]
a.0.x : 42
}
Since the `${x}` in "foo.conf" would be fixed up to `${a.0.x}' we
get our expected value of 42.
However, there are plenty of cases where the included file might However, there are plenty of cases where the included file might
intend to refer to the application's root config. For example, to intend to refer to the application's root config. For example, to
get a value from a system property or from the reference get a value from a system property or from the reference

10
NEWS.md
View File

@ -1,13 +1,3 @@
# 1.3.0: May 8, 2015
- no changes from 1.3.0-M3
- this is an ABI-guaranteed stable release.
- 1.3.0 should be ABI-compatible with 1.2.x for most applications,
though there are enough changes to corner cases and
implementation that some obscure things could break. Please see
the notes for the 1.3.0 milestones below for details, especially
for 1.3.0-M1.
# 1.3.0-M3: April 21, 2015 # 1.3.0-M3: April 21, 2015
- this is an ABI-not-guaranteed beta release in advance - this is an ABI-not-guaranteed beta release in advance

View File

@ -93,19 +93,19 @@ The license is Apache 2.0, see LICENSE-2.0.txt.
### Binary Releases ### Binary Releases
Version 1.2.1 and earlier were built for Java 6, while newer Version 1.2.1 and earlier were built for Java 6, while newer
versions (1.3.0 and above) will be built for Java 8. versions (1.3.0-M1 is the current beta) will be built for Java 8.
You can find published releases on Maven Central. You can find published releases on Maven Central.
<dependency> <dependency>
<groupId>com.typesafe</groupId> <groupId>com.typesafe</groupId>
<artifactId>config</artifactId> <artifactId>config</artifactId>
<version>1.3.0</version> <version>1.2.1</version>
</dependency> </dependency>
sbt dependency: sbt dependency:
libraryDependencies += "com.typesafe" % "config" % "1.3.0" libraryDependencies += "com.typesafe" % "config" % "1.2.1"
Link for direct download if you don't use a dependency manager: Link for direct download if you don't use a dependency manager:
@ -135,10 +135,6 @@ Typesafe Contributor License Agreement online, using your GitHub
account - it takes 30 seconds. You can do this at account - it takes 30 seconds. You can do this at
http://www.typesafe.com/contribute/cla http://www.typesafe.com/contribute/cla
Please see
[CONTRIBUTING](https://github.com/typesafehub/config/blob/master/CONTRIBUTING.md)
for more including how to make a release.
### Build ### Build
The build uses sbt and the tests are written in Scala; however, The build uses sbt and the tests are written in Scala; however,
@ -752,14 +748,6 @@ If you have trouble with your configuration, some useful tips.
Currently the library is maintained against Java 8, but Currently the library is maintained against Java 8, but
version 1.2.1 and earlier will work with Java 6. version 1.2.1 and earlier will work with Java 6.
Please use 1.2.1 if you need Java 6 support, though some people
have expressed interest in a branch off of 1.3.0 supporting
Java 7. If you want to work on that branch you might bring it up
on [chat](https://gitter.im/typesafehub/config). We can release a
jar for Java 7 if someone(s) steps up to maintain the branch. The
master branch does not use Java 8 "gratuitously" but some APIs
that use Java 8 types will need to be removed.
### Rationale for Supported File Formats ### Rationale for Supported File Formats
(For the curious.) (For the curious.)
@ -812,9 +800,6 @@ your wrapper, just send a pull request for this README. We would
love to know what you're doing with this library or with the HOCON love to know what you're doing with this library or with the HOCON
format. format.
#### Guice integration
* Typesafe Config Guice https://github.com/racc/typesafeconfig-guice
#### Scala wrappers for the Java library #### Scala wrappers for the Java library
* Ficus https://github.com/ceedubs/ficus * Ficus https://github.com/ceedubs/ficus

View File

@ -193,13 +193,6 @@ public class ConfigBeanImpl {
return config.getObjectList(configPropName); return config.getObjectList(configPropName);
} else if (elementType == ConfigValue.class) { } else if (elementType == ConfigValue.class) {
return config.getList(configPropName); return config.getList(configPropName);
} else if (hasAtLeastOneBeanProperty((Class<?>) elementType)) {
List<Object> beanList = new ArrayList<Object>();
List<? extends Config> configList = config.getConfigList(configPropName);
for (Config listMember : configList) {
beanList.add(createInternal(listMember, (Class<?>) elementType));
}
return beanList;
} else { } else {
throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has unsupported list element type " + elementType); throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has unsupported list element type " + elementType);
} }

View File

@ -270,11 +270,6 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
render(stack, sb, indent, atRoot, atKey, options); render(stack, sb, indent, atRoot, atKey, options);
} }
@Override
protected void render(StringBuilder sb, int indent, boolean atRoot, ConfigRenderOptions options) {
render(sb, indent, atRoot, null, options);
}
// static method also used by ConfigDelayedMergeObject. // static method also used by ConfigDelayedMergeObject.
static void render(List<AbstractConfigValue> stack, StringBuilder sb, int indent, boolean atRoot, String atKey, static void render(List<AbstractConfigValue> stack, StringBuilder sb, int indent, boolean atRoot, String atKey,
ConfigRenderOptions options) { ConfigRenderOptions options) {

View File

@ -32,10 +32,6 @@ final public class ConfigImplUtil {
return a.equals(b); return a.equals(b);
} }
static boolean isC0Control(int codepoint) {
return (codepoint >= 0x0000 && codepoint <= 0x001F);
}
public static String renderJsonString(String s) { public static String renderJsonString(String s) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append('"'); sb.append('"');
@ -64,7 +60,7 @@ final public class ConfigImplUtil {
sb.append("\\t"); sb.append("\\t");
break; break;
default: default:
if (isC0Control(c)) if (Character.isISOControl(c))
sb.append(String.format("\\u%04x", (int) c)); sb.append(String.format("\\u%04x", (int) c));
else else
sb.append(c); sb.append(c);

View File

@ -121,9 +121,6 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
private Collection<AbstractConfigNode> indentation() { private Collection<AbstractConfigNode> indentation() {
boolean seenNewLine = false; boolean seenNewLine = false;
ArrayList<AbstractConfigNode> indentation = new ArrayList<AbstractConfigNode>(); ArrayList<AbstractConfigNode> indentation = new ArrayList<AbstractConfigNode>();
if (children.isEmpty()) {
return indentation;
}
for (int i = 0; i < children.size(); i++) { for (int i = 0; i < children.size(); i++) {
if (!seenNewLine) { if (!seenNewLine) {
if (children.get(i) instanceof ConfigNodeSingleToken && if (children.get(i) instanceof ConfigNodeSingleToken &&
@ -144,18 +141,19 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
} }
if (indentation.isEmpty()) { if (indentation.isEmpty()) {
indentation.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " "))); indentation.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
return indentation;
} else { } else {
// Calculate the indentation of the ending curly-brace to get the indentation of the root object // Calculate the indentation of the ending curly-brace to get the indentation of the root object
AbstractConfigNode last = children.get(children.size() - 1); AbstractConfigNode last = children.get(children.size() - 1);
if (last instanceof ConfigNodeSingleToken && ((ConfigNodeSingleToken) last).token() == Tokens.CLOSE_CURLY) { if (last instanceof ConfigNodeSingleToken && ((ConfigNodeSingleToken) last).token() == Tokens.CLOSE_CURLY) {
AbstractConfigNode beforeLast = children.get(children.size() - 2); AbstractConfigNode beforeLast = children.get(children.size() - 2);
String indent = "";
if (beforeLast instanceof ConfigNodeSingleToken && if (beforeLast instanceof ConfigNodeSingleToken &&
Tokens.isIgnoredWhitespace(((ConfigNodeSingleToken) beforeLast).token())) Tokens.isIgnoredWhitespace(((ConfigNodeSingleToken) beforeLast).token())) {
indent = ((ConfigNodeSingleToken) beforeLast).token().tokenText(); String indent = ((ConfigNodeSingleToken) beforeLast).token().tokenText();
indent += " "; indent += " ";
indentation.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, indent))); indentation.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, indent)));
return indentation; return indentation;
}
} }
} }
@ -170,12 +168,12 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
// If the value we're inserting is a complex value, we'll need to indent it for insertion // If the value we're inserting is a complex value, we'll need to indent it for insertion
AbstractConfigNodeValue indentedValue; AbstractConfigNodeValue indentedValue;
if (value instanceof ConfigNodeComplexValue && !indentation.isEmpty()) { if (value instanceof ConfigNodeComplexValue) {
indentedValue = ((ConfigNodeComplexValue) value).indentText(indentation.get(indentation.size() - 1)); indentedValue = ((ConfigNodeComplexValue) value).indentText(indentation.get(indentation.size() - 1));
} else { } else {
indentedValue = value; indentedValue = value;
} }
boolean sameLine = !(indentation.size() > 0 && indentation.get(0) instanceof ConfigNodeSingleToken && boolean sameLine = !(indentation.get(0) instanceof ConfigNodeSingleToken &&
Tokens.isNewline(((ConfigNodeSingleToken) indentation.get(0)).token())); Tokens.isNewline(((ConfigNodeSingleToken) indentation.get(0)).token()));
// If the path is of length greater than one, see if the value needs to be added further down // If the path is of length greater than one, see if the value needs to be added further down
@ -211,10 +209,7 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
// If the path is of length greater than one add the required new objects along the path // If the path is of length greater than one add the required new objects along the path
ArrayList<AbstractConfigNode> newObjectNodes = new ArrayList<AbstractConfigNode>(); ArrayList<AbstractConfigNode> newObjectNodes = new ArrayList<AbstractConfigNode>();
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.OPEN_CURLY)); newObjectNodes.add(new ConfigNodeSingleToken(Tokens.OPEN_CURLY));
if (indentation.isEmpty()) { newObjectNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
}
newObjectNodes.addAll(indentation);
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.CLOSE_CURLY)); newObjectNodes.add(new ConfigNodeSingleToken(Tokens.CLOSE_CURLY));
ConfigNodeObject newObject = new ConfigNodeObject(newObjectNodes); ConfigNodeObject newObject = new ConfigNodeObject(newObjectNodes);
newNodes.add(newObject.addValueOnPath(desiredPath.subPath(1), indentedValue, flavor)); newNodes.add(newObject.addValueOnPath(desiredPath.subPath(1), indentedValue, flavor));

View File

@ -34,11 +34,6 @@ final class ConfigParser {
final private ConfigOrigin baseOrigin; final private ConfigOrigin baseOrigin;
final private LinkedList<Path> pathStack; final private LinkedList<Path> pathStack;
// the number of lists we are inside; this is used to detect the "cannot
// generate a reference to a list element" problem, and once we fix that
// problem we should be able to get rid of this variable.
int arrayCount;
ParseContext(ConfigSyntax flavor, ConfigOrigin origin, ConfigNodeRoot document, ParseContext(ConfigSyntax flavor, ConfigOrigin origin, ConfigNodeRoot document,
FullIncluder includer, ConfigIncludeContext includeContext) { FullIncluder includer, ConfigIncludeContext includeContext) {
lineNumber = 1; lineNumber = 1;
@ -48,7 +43,6 @@ final class ConfigParser {
this.includer = includer; this.includer = includer;
this.includeContext = includeContext; this.includeContext = includeContext;
this.pathStack = new LinkedList<Path>(); this.pathStack = new LinkedList<Path>();
this.arrayCount = 0;
} }
// merge a bunch of adjacent values into one // merge a bunch of adjacent values into one
@ -95,8 +89,6 @@ final class ConfigParser {
private AbstractConfigValue parseValue(AbstractConfigNodeValue n, List<String> comments) { private AbstractConfigValue parseValue(AbstractConfigNodeValue n, List<String> comments) {
AbstractConfigValue v; AbstractConfigValue v;
int startingArrayCount = arrayCount;
if (n instanceof ConfigNodeSimpleValue) { if (n instanceof ConfigNodeSimpleValue) {
v = ((ConfigNodeSimpleValue) n).value(); v = ((ConfigNodeSimpleValue) n).value();
} else if (n instanceof ConfigNodeObject) { } else if (n instanceof ConfigNodeObject) {
@ -114,9 +106,6 @@ final class ConfigParser {
comments.clear(); comments.clear();
} }
if (arrayCount != startingArrayCount)
throw new ConfigException.BugOrBroken("Bug in config parser: unbalanced array count");
return v; return v;
} }
@ -187,14 +176,6 @@ final class ConfigParser {
throw new ConfigException.BugOrBroken("should not be reached"); throw new ConfigException.BugOrBroken("should not be reached");
} }
// we really should make this work, but for now throwing an
// exception is better than producing an incorrect result.
// See https://github.com/typesafehub/config/issues/160
if (arrayCount > 0 && obj.resolveStatus() != ResolveStatus.RESOLVED)
throw parseError("Due to current limitations of the config parser, when an include statement is nested inside a list value, "
+ "${} substitutions inside the included file cannot be resolved correctly. Either move the include outside of the list value or "
+ "remove the ${} statements from the included file.");
if (!pathStack.isEmpty()) { if (!pathStack.isEmpty()) {
Path prefix = fullCurrentPath(); Path prefix = fullCurrentPath();
obj = obj.relativized(prefix); obj = obj.relativized(prefix);
@ -240,21 +221,6 @@ final class ConfigParser {
// path must be on-stack while we parse the value // path must be on-stack while we parse the value
pathStack.push(path); pathStack.push(path);
if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) {
// we really should make this work, but for now throwing
// an exception is better than producing an incorrect
// result. See
// https://github.com/typesafehub/config/issues/160
if (arrayCount > 0)
throw parseError("Due to current limitations of the config parser, += does not work nested inside a list. "
+ "+= expands to a ${} substitution and the path in ${} cannot currently refer to list elements. "
+ "You might be able to move the += outside of the list and then refer to it from inside the list with ${}.");
// because we will put it in an array after the fact so
// we want this to be incremented during the parseValue
// below in order to throw the above exception.
arrayCount += 1;
}
AbstractConfigNodeValue valueNode; AbstractConfigNodeValue valueNode;
AbstractConfigValue newValue; AbstractConfigValue newValue;
@ -265,7 +231,6 @@ final class ConfigParser {
newValue = parseValue(valueNode, comments); newValue = parseValue(valueNode, comments);
if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) { if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) {
arrayCount -= 1;
List<AbstractConfigValue> concat = new ArrayList<AbstractConfigValue>(2); List<AbstractConfigValue> concat = new ArrayList<AbstractConfigValue>(2);
AbstractConfigValue previousRef = new ConfigReference(newValue.origin(), AbstractConfigValue previousRef = new ConfigReference(newValue.origin(),
@ -346,7 +311,6 @@ final class ConfigParser {
} }
private SimpleConfigList parseArray(ConfigNodeArray n) { private SimpleConfigList parseArray(ConfigNodeArray n) {
arrayCount += 1;
SimpleConfigOrigin arrayOrigin = lineOrigin(); SimpleConfigOrigin arrayOrigin = lineOrigin();
List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>(); List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();
@ -356,6 +320,8 @@ final class ConfigParser {
AbstractConfigValue v = null; AbstractConfigValue v = null;
int index = 0;
for (AbstractConfigNode node : n.children()) { for (AbstractConfigNode node : n.children()) {
if (node instanceof ConfigNodeComment) { if (node instanceof ConfigNodeComment) {
comments.add(((ConfigNodeComment) node).commentText()); comments.add(((ConfigNodeComment) node).commentText());
@ -376,14 +342,16 @@ final class ConfigParser {
values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments)))); values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments))));
comments.clear(); comments.clear();
} }
pathStack.push(new Path(Integer.toString(index)));
index ++;
v = parseValue((AbstractConfigNodeValue)node, comments); v = parseValue((AbstractConfigNodeValue)node, comments);
pathStack.pop();
} }
} }
// There shouldn't be any comments at this point, but add them just in case // There shouldn't be any comments at this point, but add them just in case
if (v != null) { if (v != null) {
values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments)))); values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments))));
} }
arrayCount -= 1;
return new SimpleConfigList(arrayOrigin, values); return new SimpleConfigList(arrayOrigin, values);
} }

View File

@ -55,7 +55,7 @@ final class ResolveSource {
} }
} }
static private ValueWithPath findInObject(AbstractConfigObject obj, Path path) { static private ValueWithPath findInObject(AbstractConfigValue obj, Path path) {
try { try {
// we'll fail if anything along the path can't // we'll fail if anything along the path can't
// be looked at without resolving. // be looked at without resolving.
@ -65,19 +65,34 @@ final class ResolveSource {
} }
} }
static private ValueWithPath findInObject(AbstractConfigObject obj, Path path, Node<Container> parents) { static private ValueWithPath findInObject(AbstractConfigValue obj, Path path, Node<Container> parents) {
String key = path.first(); String key = path.first();
Path next = path.remainder(); Path next = path.remainder();
if (ConfigImpl.traceSubstitutionsEnabled()) if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("*** looking up '" + key + "' in " + obj); ConfigImpl.trace("*** looking up '" + key + "' in " + obj);
AbstractConfigValue v = obj.attemptPeekWithPartialResolve(key);
Node<Container> newParents = parents == null ? new Node<Container>(obj) : parents.prepend(obj); AbstractConfigValue v = null;
Node<Container> newParents = null;
if (obj instanceof AbstractConfigObject) {
v = ((AbstractConfigObject) obj).attemptPeekWithPartialResolve(key);
newParents = parents == null ? new Node<Container>((AbstractConfigObject) obj) : parents.prepend((AbstractConfigObject) obj);
} else if (obj instanceof SimpleConfigList) {
int ix;
try {
ix = Integer.parseInt(key);
} catch(Exception e) {
throw new ConfigException.BadPath(key, "path for list must be numeric index");
}
v = ((SimpleConfigList) obj).get(ix);
newParents = parents == null ? new Node<Container>((SimpleConfigList) obj) : parents.prepend((SimpleConfigList) obj);
}
if (next == null) { if (next == null) {
return new ValueWithPath(v, newParents); return new ValueWithPath(v, newParents);
} else { } else {
if (v instanceof AbstractConfigObject) { if (v instanceof AbstractConfigObject || v instanceof SimpleConfigList) {
return findInObject((AbstractConfigObject) v, next, newParents); return findInObject(v, next, newParents);
} else { } else {
return new ValueWithPath(null, newParents); return new ValueWithPath(null, newParents);
} }

View File

@ -1,7 +1,9 @@
package com.typesafe.config.impl; package com.typesafe.config.impl;
import com.typesafe.config.*;
import com.typesafe.config.parser.ConfigDocument; import com.typesafe.config.parser.ConfigDocument;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigException;
import java.io.StringReader; import java.io.StringReader;
import java.util.Iterator; import java.util.Iterator;
@ -32,9 +34,7 @@ final class SimpleConfigDocument implements ConfigDocument {
public ConfigDocument withValue(String path, ConfigValue newValue) { public ConfigDocument withValue(String path, ConfigValue newValue) {
if (newValue == null) if (newValue == null)
throw new ConfigException.BugOrBroken("null value for " + path + " passed to withValue"); throw new ConfigException.BugOrBroken("null value for " + path + " passed to withValue");
ConfigRenderOptions options = ConfigRenderOptions.defaults(); return withValueText(path, newValue.render().trim());
options = options.setOriginComments(false);
return withValueText(path, newValue.render(options).trim());
} }
@Override @Override

View File

@ -38,7 +38,7 @@ final class Tokenizer {
return "tab"; return "tab";
else if (codepoint == -1) else if (codepoint == -1)
return "end of file"; return "end of file";
else if (ConfigImplUtil.isC0Control(codepoint)) else if (Character.isISOControl(codepoint))
return String.format("control character 0x%x", codepoint); return String.format("control character 0x%x", codepoint);
else else
return String.format("%c", codepoint); return String.format("%c", codepoint);
@ -498,7 +498,7 @@ final class Tokenizer {
} else if (c == '"') { } else if (c == '"') {
sbOrig.appendCodePoint(c); sbOrig.appendCodePoint(c);
break; break;
} else if (ConfigImplUtil.isC0Control(c)) { } else if (Character.isISOControl(c)) {
throw problem(asString(c), "JSON does not allow unescaped " + asString(c) throw problem(asString(c), "JSON does not allow unescaped " + asString(c)
+ " in quoted strings, use a backslash escape"); + " in quoted strings, use a backslash escape");
} else { } else {

View File

@ -23,7 +23,6 @@ public class ArraysConfig {
List<ConfigValue> ofConfigValue; List<ConfigValue> ofConfigValue;
List<Duration> ofDuration; List<Duration> ofDuration;
List<ConfigMemorySize> ofMemorySize; List<ConfigMemorySize> ofMemorySize;
List<StringsConfig> ofStringBean;
public List<Integer> getEmpty() { public List<Integer> getEmpty() {
return empty; return empty;
@ -128,12 +127,4 @@ public class ArraysConfig {
public void setOfMemorySize(List<ConfigMemorySize> ofMemorySize) { public void setOfMemorySize(List<ConfigMemorySize> ofMemorySize) {
this.ofMemorySize = ofMemorySize; this.ofMemorySize = ofMemorySize;
} }
public List<StringsConfig> getOfStringBean() {
return ofStringBean;
}
public void setOfStringBean(List<StringsConfig> ofStringBean) {
this.ofStringBean = ofStringBean;
}
} }

View File

@ -20,26 +20,4 @@ public class StringsConfig {
public void setYes(String s) { public void setYes(String s) {
yes = s; yes = s;
} }
@Override
public boolean equals(Object o) {
if (o instanceof StringsConfig) {
StringsConfig sc = (StringsConfig) o;
return sc.abcd.equals(abcd) &&
sc.yes.equals(yes);
} else {
return false;
}
}
@Override
public int hashCode() {
int h = 41 * (41 + abcd.hashCode());
return h + yes.hashCode();
}
@Override
public String toString() {
return "StringsConfig(" + abcd + "," + yes + ")";
}
} }

View File

@ -52,17 +52,7 @@
"ofConfigObject" : [${numbers}, ${booleans}, ${strings}], "ofConfigObject" : [${numbers}, ${booleans}, ${strings}],
"ofConfigValue" : [1, 2, "a"], "ofConfigValue" : [1, 2, "a"],
"ofDuration" : [1, 2h, 3 days], "ofDuration" : [1, 2h, 3 days],
"ofMemorySize" : [1024, 1M, 1G], "ofMemorySize" : [1024, 1M, 1G]
"ofStringBean" : [
{
abcd : "testAbcdOne"
yes : "testYesOne"
},
{
abcd : "testAbcdTwo"
yes : "testYesTwo"
}
]
}, },
"bytes" : { "bytes" : {
"kilobyte" : "1kB", "kilobyte" : "1kB",

View File

@ -1,5 +1,10 @@
// The {} inside the [] is needed because // The {} inside the [] is needed because
// just [ include ] means an array with the // just [ include ] means an array with the
// string "include" in it. // string "include" in it.
a = [ { include "test01.conf" } ] a = [
{
include "test12.conf"
replace-key: "replaced"
}
]

View File

@ -0,0 +1,3 @@
// This file is included by include-from-list.conf to verify that includes with substitutions work in arrays
replace-me: ${replace-key}

View File

@ -345,26 +345,19 @@ class ConcatenationTest extends TestUtils {
assertEquals(Seq(1, 2, 3), conf.getObjectList("x.a").asScala.toList.map(_.toConfig.getInt("b"))) assertEquals(Seq(1, 2, 3), conf.getObjectList("x.a").asScala.toList.map(_.toConfig.getInt("b")))
} }
// We would ideally make this case NOT throw an exception but we need to do some work
// to get there, see https://github.com/typesafehub/config/issues/160
@Test @Test
def plusEqualsMultipleTimesNestedInArray() { def plusEqualsMultipleTimesNestedInArray() {
val e = intercept[ConfigException.Parse] { val conf = parseConfig("""x = [ { a += 1, a += 2, a += 3 } ] """).resolve()
val conf = parseConfig("""x = [ { a += 1, a += 2, a += 3 } ] """).resolve() assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList)
assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList)
}
assertTrue(e.getMessage.contains("limitation"))
} }
// We would ideally make this case NOT throw an exception but we need to do some work
// to get there, see https://github.com/typesafehub/config/issues/160
@Test @Test
def plusEqualsMultipleTimesNestedInPlusEquals() { def plusEqualsMultipleTimesNestedInPlusEquals() {
val e = intercept[ConfigException.Parse] { val e = intercept[ConfigException.BugOrBroken] {
val conf = parseConfig("""x += { a += 1, a += 2, a += 3 } """).resolve() val conf = parseConfig("""x += { a += 1, a += 2, a += 3 } """).resolve()
assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList) assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList)
} }
assertTrue(e.getMessage.contains("limitation")) assertTrue(e.getMessage.contains("did not find"))
} }
// from https://github.com/typesafehub/config/issues/177 // from https://github.com/typesafehub/config/issues/177

View File

@ -801,6 +801,14 @@ class ConfParserTest extends TestUtils {
assertTrue("including basename URL doesn't load anything", conf.isEmpty()) assertTrue("including basename URL doesn't load anything", conf.isEmpty())
} }
@Test
def includeWithSubstitutionsFromList() {
val conf = ConfigFactory.parseString("include file(" + jsonQuotedResourceFile("include-from-list") + ")")
val resolved = conf.resolve()
assertEquals(resolved.getConfigList("a").get(0).getString("replace-me"), "replaced")
}
@Test @Test
def acceptBOMStartingFile() { def acceptBOMStartingFile() {
// BOM at start of file should be ignored // BOM at start of file should be ignored

View File

@ -46,10 +46,10 @@ class ConfigBeanFactoryTest extends TestUtils {
ConfigBeanFactory.create(config, classOf[ValidationBeanConfig]) ConfigBeanFactory.create(config, classOf[ValidationBeanConfig])
} }
val expecteds = Seq(Missing("propNotListedInConfig", 77, "string"), val expecteds = Seq(Missing("propNotListedInConfig", 67, "string"),
WrongType("shouldBeInt", 78, "number", "boolean"), WrongType("shouldBeInt", 68, "number", "boolean"),
WrongType("should-be-boolean", 79, "boolean", "number"), WrongType("should-be-boolean", 69, "boolean", "number"),
WrongType("should-be-list", 80, "list", "string")) WrongType("should-be-list", 70, "list", "string"))
checkValidationException(e, expecteds) checkValidationException(e, expecteds)
} }
@ -111,15 +111,6 @@ class ConfigBeanFactoryTest extends TestUtils {
ConfigMemorySize.ofBytes(1048576), ConfigMemorySize.ofBytes(1048576),
ConfigMemorySize.ofBytes(1073741824)), ConfigMemorySize.ofBytes(1073741824)),
beanConfig.getOfMemorySize.asScala) beanConfig.getOfMemorySize.asScala)
val stringsConfigOne = new StringsConfig();
stringsConfigOne.setAbcd("testAbcdOne")
stringsConfigOne.setYes("testYesOne")
val stringsConfigTwo = new StringsConfig();
stringsConfigTwo.setAbcd("testAbcdTwo")
stringsConfigTwo.setYes("testYesTwo")
assertEquals(List(stringsConfigOne, stringsConfigTwo).asJava, beanConfig.getOfStringBean)
} }
@Test @Test

View File

@ -171,14 +171,14 @@ class ConfigDocumentTest extends TestUtils {
@Test @Test
def configDocumentSetNewValueMultiLevelConf { def configDocumentSetNewValueMultiLevelConf {
val origText = "a:b\nc:d" val origText = "a:b\nc:d"
val finalText = "a:b\nc:d\ne : {\n f : {\n g : 12\n }\n}" val finalText = "a:b\nc:d\ne : { f : { g : 12 } }"
configDocumentReplaceConfTest(origText, finalText, "12", "e.f.g") configDocumentReplaceConfTest(origText, finalText, "12", "e.f.g")
} }
@Test @Test
def configDocumentSetNewValueMultiLevelJson { def configDocumentSetNewValueMultiLevelJson {
val origText = "{\"a\":\"b\",\n\"c\":\"d\"}" val origText = "{\"a\":\"b\",\n\"c\":\"d\"}"
val finalText = "{\"a\":\"b\",\n\"c\":\"d\",\n \"e\" : {\n \"f\" : {\n \"g\" : 12\n }\n }}" val finalText = "{\"a\":\"b\",\n\"c\":\"d\",\n\"e\" : { \"f\" : { \"g\" : 12 } }}"
configDocumentReplaceJsonTest(origText, finalText, "12", "e.f.g") configDocumentReplaceJsonTest(origText, finalText, "12", "e.f.g")
} }
@ -342,13 +342,13 @@ class ConfigDocumentTest extends TestUtils {
var configDocument = ConfigDocumentFactory.parseString(origText) var configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b: c\n e : f\n}", configDocument.withValueText("a.e", "f").render()) assertEquals("a {\n b: c\n e : f\n}", configDocument.withValueText("a.e", "f").render())
assertEquals("a {\n b: c\n d : {\n e : {\n f : g\n }\n }\n}", configDocument.withValueText("a.d.e.f", "g").render()) assertEquals("a {\n b: c\n d : { e : { f : g } }\n}", configDocument.withValueText("a.d.e.f", "g").render())
origText = "a {\n b: c\n}\n" origText = "a {\n b: c\n}\n"
configDocument = ConfigDocumentFactory.parseString(origText) configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b: c\n}\nd : e\n", configDocument.withValueText("d", "e").render()) assertEquals("a {\n b: c\n}\nd : e\n", configDocument.withValueText("d", "e").render())
assertEquals("a {\n b: c\n}\nd : {\n e : {\n f : g\n }\n}\n", configDocument.withValueText("d.e.f", "g").render()) assertEquals("a {\n b: c\n}\nd : { e : { f : g } }\n", configDocument.withValueText("d.e.f", "g").render())
} }
@Test @Test
@ -435,14 +435,7 @@ class ConfigDocumentTest extends TestUtils {
val origText = "" val origText = ""
val configDocument = ConfigDocumentFactory.parseString(origText) val configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a : 1", configDocument.withValueText("a", "1").render) assertEquals(" a : 1", configDocument.withValueText("a", "1").render)
val mapVal = ConfigValueFactory.fromAnyRef(Map("a" -> 1, "b" -> 2).asJava)
assertEquals("a : {\n \"a\" : 1,\n \"b\" : 2\n}",
configDocument.withValue("a", mapVal).render)
val arrayVal = ConfigValueFactory.fromAnyRef(List(1, 2).asJava)
assertEquals("a : [\n 1,\n 2\n]", configDocument.withValue("a", arrayVal).render)
} }
@Test @Test
@ -452,7 +445,9 @@ class ConfigDocumentTest extends TestUtils {
val configVal = ConfigValueFactory.fromAnyRef(Map("a" -> 1, "b" -> 2).asJava) val configVal = ConfigValueFactory.fromAnyRef(Map("a" -> 1, "b" -> 2).asJava)
assertEquals("{ a : {\n \"a\" : 1,\n \"b\" : 2\n } }", assertEquals("{ a : {\n # hardcoded value\n \"a\" : 1,\n # hardcoded value\n \"b\" : 2\n } }",
configDocument.withValue("a", configVal).render) configDocument.withValue("a", configVal).render)
} }
} }

View File

@ -212,7 +212,7 @@ class ConfigNodeTest extends TestUtils {
nodeCloseBrace)) nodeCloseBrace))
assertEquals(origText, origNode.render()) assertEquals(origText, origNode.render())
val finalText = "foo : bar\nbaz : {\n\t\"abc.def\" : true\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" + val finalText = "foo : bar\nbaz : {\n\t\"abc.def\" : true\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" +
"def : false\n\t\t\n\t\t\"this.does.not.exist@@@+$#\" : {\n\t\t end : doesnotexist\n\t\t}\n\t}\n}\n\nbaz.abc.ghi : randomunquotedString\n}" "def : false\n\t\t\n\t\t\"this.does.not.exist@@@+$#\" : { end : doesnotexist }\n\t}\n}\n\nbaz.abc.ghi : randomunquotedString\n}"
//Can replace settings in nested maps //Can replace settings in nested maps
// Paths with quotes in the name are treated as a single Path, rather than multiple sub-paths // Paths with quotes in the name are treated as a single Path, rather than multiple sub-paths

View File

@ -875,17 +875,6 @@ class PublicApiTest extends TestUtils {
assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("include statements nested")) assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("include statements nested"))
} }
// We would ideally make this case NOT throw an exception but we need to do some work
// to get there, see https://github.com/typesafehub/config/issues/160
@Test
def detectIncludeFromList() {
val e = intercept[ConfigException.Parse] {
ConfigFactory.load("include-from-list.conf")
}
assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("limitation"))
}
@Test @Test
def missingOverrideResourceFails() { def missingOverrideResourceFails() {
assertEquals("config.file is not set", null, System.getProperty("config.file")) assertEquals("config.file is not set", null, System.getProperty("config.file"))