From 31e2cb354f1f5b3cbec4bfa94e2d9d9075fb0d24 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Tue, 13 Dec 2011 15:28:14 +0100 Subject: [PATCH] Updated to latest config release from typesafehub, v0.1.8 --- .../main/java/com/typesafe/config/Config.java | 26 ++- .../com/typesafe/config/ConfigFactory.java | 4 +- .../com/typesafe/config/ConfigObject.java | 18 +- .../java/com/typesafe/config/ConfigUtil.java | 70 +++++++ .../config/impl/AbstractConfigValue.java | 4 +- .../config/impl/ConfigDelayedMerge.java | 4 +- .../com/typesafe/config/impl/ConfigImpl.java | 92 +++++---- .../{ConfigUtil.java => ConfigImplUtil.java} | 40 +++- .../typesafe/config/impl/ConfigString.java | 2 +- .../config/impl/ConfigSubstitution.java | 2 +- .../com/typesafe/config/impl/Parseable.java | 31 ++- .../java/com/typesafe/config/impl/Parser.java | 180 +++++++++++++---- .../java/com/typesafe/config/impl/Path.java | 4 +- .../typesafe/config/impl/SimpleConfig.java | 46 ++++- .../config/impl/SimpleConfigOrigin.java | 6 +- .../java/com/typesafe/config/impl/Token.java | 47 ++++- .../com/typesafe/config/impl/TokenType.java | 15 +- .../com/typesafe/config/impl/Tokenizer.java | 159 +++++++++------ .../java/com/typesafe/config/impl/Tokens.java | 186 +++++++++++------- akka-docs/general/configuration.rst | 4 +- 20 files changed, 700 insertions(+), 240 deletions(-) create mode 100644 akka-actor/src/main/java/com/typesafe/config/ConfigUtil.java rename akka-actor/src/main/java/com/typesafe/config/impl/{ConfigUtil.java => ConfigImplUtil.java} (78%) diff --git a/akka-actor/src/main/java/com/typesafe/config/Config.java b/akka-actor/src/main/java/com/typesafe/config/Config.java index 1c7fca50e5..44eebe1158 100644 --- a/akka-actor/src/main/java/com/typesafe/config/Config.java +++ b/akka-actor/src/main/java/com/typesafe/config/Config.java @@ -4,6 +4,8 @@ package com.typesafe.config; import java.util.List; +import java.util.Map; +import java.util.Set; /** * An immutable map from config paths to config values. @@ -32,6 +34,10 @@ import java.util.List; * {@code ConfigObject} is a tree of nested maps from keys to values. * *

+ * Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert + * between path expressions and individual path elements (keys). + * + *

* Another difference between {@code Config} and {@code ConfigObject} is that * conceptually, {@code ConfigValue}s with a {@link ConfigValue#valueType() * valueType()} of {@link ConfigValueType#NULL NULL} exist in a @@ -54,10 +60,11 @@ import java.util.List; * are performed for you though. * *

- * If you want to iterate over the contents of a {@code Config}, you have to get - * its {@code ConfigObject} with {@link #root()}, and then iterate over the - * {@code ConfigObject}. - * + * If you want to iterate over the contents of a {@code Config}, you can get its + * {@code ConfigObject} with {@link #root()}, and then iterate over the + * {@code ConfigObject} (which implements java.util.Map). Or, you + * can use {@link #entrySet()} which recurses the object tree for you and builds + * up a Set of all path-value pairs where the value is not null. * *

* Do not implement {@code Config}; it should only be implemented by @@ -256,6 +263,17 @@ public interface Config extends ConfigMergeable { */ boolean isEmpty(); + /** + * Returns the set of path-value pairs, excluding any null values, found by + * recursing {@link #root() the root object}. Note that this is very + * different from root().entrySet() which returns the set of + * immediate-child keys in the root object and includes null values. + * + * @return set of paths with non-null values, built up by recursing the + * entire tree of {@link ConfigObject} + */ + Set> entrySet(); + /** * * @param path diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java b/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java index 9251b3fb45..dc851d7f2b 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java @@ -11,7 +11,7 @@ import java.util.Map; import java.util.Properties; import com.typesafe.config.impl.ConfigImpl; -import com.typesafe.config.impl.ConfigUtil; +import com.typesafe.config.impl.ConfigImplUtil; import com.typesafe.config.impl.Parseable; /** @@ -179,7 +179,7 @@ public final class ConfigFactory { try { return DefaultConfigHolder.defaultConfig; } catch (ExceptionInInitializerError e) { - throw ConfigUtil.extractInitializerError(e); + throw ConfigImplUtil.extractInitializerError(e); } } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java b/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java index 8613840223..54cce1c39f 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java @@ -8,34 +8,38 @@ import java.util.Map; /** * Subtype of {@link ConfigValue} representing an object (dictionary, map) * value, as in JSON's { "a" : 42 } syntax. - * + * *

* {@code ConfigObject} implements {@code java.util.Map} so * you can use it like a regular Java map. Or call {@link #unwrapped()} to * unwrap the map to a map with plain Java values rather than * {@code ConfigValue}. - * + * *

* Like all {@link ConfigValue} subtypes, {@code ConfigObject} is immutable. * This makes it threadsafe and you never have to create "defensive copies." The * mutator methods from {@link java.util.Map} all throw * {@link java.lang.UnsupportedOperationException}. - * + * *

* The {@link ConfigValue#valueType} method on an object returns * {@link ConfigValueType#OBJECT}. - * + * *

* In most cases you want to use the {@link Config} interface rather than this * one. Call {@link #toConfig()} to convert a {@code ConfigObject} to a * {@code Config}. - * + * *

* The API for a {@code ConfigObject} is in terms of keys, while the API for a * {@link Config} is in terms of path expressions. Conceptually, * {@code ConfigObject} is a tree of maps from keys to values, while a * {@code ConfigObject} is a one-level map from paths to values. - * + * + *

+ * Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert + * between path expressions and individual path elements (keys). + * *

* A {@code ConfigObject} may contain null values, which will have * {@link ConfigValue#valueType()} equal to {@link ConfigValueType#NULL}. If @@ -43,7 +47,7 @@ import java.util.Map; * file (or wherever this value tree came from). If {@code get()} returns a * {@link ConfigValue} with type {@code ConfigValueType#NULL} then the key was * set to null explicitly in the config file. - * + * *

* Do not implement {@code ConfigObject}; it should only be implemented * by the config library. Arbitrary implementations will not work because the diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigUtil.java b/akka-actor/src/main/java/com/typesafe/config/ConfigUtil.java new file mode 100644 index 0000000000..1aa463f46c --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigUtil.java @@ -0,0 +1,70 @@ +package com.typesafe.config; + +import java.util.List; + +import com.typesafe.config.impl.ConfigImplUtil; + +public final class ConfigUtil { + private ConfigUtil() { + + } + + /** + * Quotes and escapes a string, as in the JSON specification. + * + * @param s + * a string + * @return the string quoted and escaped + */ + public static String quoteString(String s) { + return ConfigImplUtil.renderJsonString(s); + } + + /** + * Converts a list of keys to a path expression, by quoting the path + * elements as needed and then joining them separated by a period. A path + * expression is usable with a {@link Config}, while individual path + * elements are usable with a {@link ConfigObject}. + * + * @param elements + * the keys in the path + * @return a path expression + * @throws ConfigException + * if there are no elements + */ + public static String joinPath(String... elements) { + return ConfigImplUtil.joinPath(elements); + } + + /** + * Converts a list of strings to a path expression, by quoting the path + * elements as needed and then joining them separated by a period. A path + * expression is usable with a {@link Config}, while individual path + * elements are usable with a {@link ConfigObject}. + * + * @param elements + * the keys in the path + * @return a path expression + * @throws ConfigException + * if the list is empty + */ + public static String joinPath(List elements) { + return ConfigImplUtil.joinPath(elements); + } + + /** + * Converts a path expression into a list of keys, by splitting on period + * and unquoting the individual path elements. A path expression is usable + * with a {@link Config}, while individual path elements are usable with a + * {@link ConfigObject}. + * + * @param path + * a path expression + * @return the individual keys in the path + * @throws ConfigException + * if the path expression is invalid + */ + public static List splitPath(String path) { + return ConfigImplUtil.splitPath(path); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java index 1bec6ec536..68ab5cc316 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java @@ -144,7 +144,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue { return canEqual(other) && (this.valueType() == ((ConfigValue) other).valueType()) - && ConfigUtil.equalsHandlingNull(this.unwrapped(), + && ConfigImplUtil.equalsHandlingNull(this.unwrapped(), ((ConfigValue) other).unwrapped()); } else { return false; @@ -178,7 +178,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue { protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) { if (atKey != null) { - sb.append(ConfigUtil.renderJsonString(atKey)); + sb.append(ConfigImplUtil.renderJsonString(atKey)); sb.append(" : "); } render(sb, indent, formatted); diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java index b916d9a0a7..9846cc57f2 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java @@ -189,7 +189,7 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements indent(sb, indent); if (atKey != null) { sb.append("# unmerged value " + i + " for key " - + ConfigUtil.renderJsonString(atKey) + " from "); + + ConfigImplUtil.renderJsonString(atKey) + " from "); } else { sb.append("# unmerged value " + i + " from "); } @@ -200,7 +200,7 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements } if (atKey != null) { - sb.append(ConfigUtil.renderJsonString(atKey)); + sb.append(ConfigImplUtil.renderJsonString(atKey)); sb.append(" : "); } v.render(sb, indent, formatted); diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java index 8c016d6f98..217f4385e9 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java @@ -40,47 +40,66 @@ public class ConfigImpl { || name.endsWith(".properties")) { ConfigParseable p = source.nameToParseable(name); - if (p != null) { - obj = p.parse(p.options().setAllowMissing( - options.getAllowMissing())); - } else { - obj = SimpleConfigObject.emptyMissing(SimpleConfigOrigin.newSimple(name)); - } + obj = p.parse(p.options().setAllowMissing(options.getAllowMissing())); } else { ConfigParseable confHandle = source.nameToParseable(name + ".conf"); ConfigParseable jsonHandle = source.nameToParseable(name + ".json"); ConfigParseable propsHandle = source.nameToParseable(name + ".properties"); - - if (!options.getAllowMissing() && confHandle == null - && jsonHandle == null && propsHandle == null) { - throw new ConfigException.IO(SimpleConfigOrigin.newSimple(name), - "No config files {.conf,.json,.properties} found"); - } + boolean gotSomething = false; + List failMessages = new ArrayList(); ConfigSyntax syntax = options.getSyntax(); obj = SimpleConfigObject.empty(SimpleConfigOrigin.newSimple(name)); - if (confHandle != null - && (syntax == null || syntax == ConfigSyntax.CONF)) { - obj = confHandle.parse(confHandle.options() - .setAllowMissing(true).setSyntax(ConfigSyntax.CONF)); + if (syntax == null || syntax == ConfigSyntax.CONF) { + try { + obj = confHandle.parse(confHandle.options().setAllowMissing(false) + .setSyntax(ConfigSyntax.CONF)); + gotSomething = true; + } catch (ConfigException.IO e) { + failMessages.add(e.getMessage()); + } } - if (jsonHandle != null - && (syntax == null || syntax == ConfigSyntax.JSON)) { - ConfigObject parsed = jsonHandle.parse(jsonHandle - .options().setAllowMissing(true) - .setSyntax(ConfigSyntax.JSON)); - obj = obj.withFallback(parsed); + if (syntax == null || syntax == ConfigSyntax.JSON) { + try { + ConfigObject parsed = jsonHandle.parse(jsonHandle.options() + .setAllowMissing(false).setSyntax(ConfigSyntax.JSON)); + obj = obj.withFallback(parsed); + gotSomething = true; + } catch (ConfigException.IO e) { + failMessages.add(e.getMessage()); + } } - if (propsHandle != null - && (syntax == null || syntax == ConfigSyntax.PROPERTIES)) { - ConfigObject parsed = propsHandle.parse(propsHandle.options() - .setAllowMissing(true) - .setSyntax(ConfigSyntax.PROPERTIES)); - obj = obj.withFallback(parsed); + if (syntax == null || syntax == ConfigSyntax.PROPERTIES) { + try { + ConfigObject parsed = propsHandle.parse(propsHandle.options() + .setAllowMissing(false).setSyntax(ConfigSyntax.PROPERTIES)); + obj = obj.withFallback(parsed); + gotSomething = true; + } catch (ConfigException.IO e) { + failMessages.add(e.getMessage()); + } + } + + if (!options.getAllowMissing() && !gotSomething) { + String failMessage; + if (failMessages.isEmpty()) { + // this should not happen + throw new ConfigException.BugOrBroken( + "should not be reached: nothing found but no exceptions thrown"); + } else { + StringBuilder sb = new StringBuilder(); + for (String msg : failMessages) { + sb.append(msg); + sb.append(", "); + } + sb.setLength(sb.length() - 2); + failMessage = sb.toString(); + } + throw new ConfigException.IO(SimpleConfigOrigin.newSimple(name), failMessage); } } @@ -269,7 +288,14 @@ public class ConfigImpl { NameSource source = new NameSource() { @Override public ConfigParseable nameToParseable(String name) { - return context.relativeTo(name); + ConfigParseable p = context.relativeTo(name); + if (p == null) { + // avoid returning null + return Parseable.newNotFound(name, "include was not found: '" + name + "'", + ConfigParseOptions.defaults()); + } else { + return p; + } } }; @@ -308,7 +334,7 @@ public class ConfigImpl { try { return DefaultIncluderHolder.defaultIncluder; } catch (ExceptionInInitializerError e) { - throw ConfigUtil.extractInitializerError(e); + throw ConfigImplUtil.extractInitializerError(e); } } @@ -326,7 +352,7 @@ public class ConfigImpl { try { return SystemPropertiesHolder.systemProperties; } catch (ExceptionInInitializerError e) { - throw ConfigUtil.extractInitializerError(e); + throw ConfigImplUtil.extractInitializerError(e); } } @@ -362,7 +388,7 @@ public class ConfigImpl { try { return EnvVariablesHolder.envVariables; } catch (ExceptionInInitializerError e) { - throw ConfigUtil.extractInitializerError(e); + throw ConfigImplUtil.extractInitializerError(e); } } @@ -384,7 +410,7 @@ public class ConfigImpl { try { return ReferenceHolder.referenceConfig; } catch (ExceptionInInitializerError e) { - throw ConfigUtil.extractInitializerError(e); + throw ConfigImplUtil.extractInitializerError(e); } } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java similarity index 78% rename from akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java rename to akka-actor/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java index 6f7b2c5aaa..cbc0ecca09 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java @@ -6,12 +6,14 @@ package com.typesafe.config.impl; import java.io.File; import java.net.URISyntaxException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import com.typesafe.config.ConfigException; /** This is public just for the "config" package to use, don't touch it */ -final public class ConfigUtil { +final public class ConfigImplUtil { static boolean equalsHandlingNull(Object a, Object b) { if (a == null && b != null) return false; @@ -23,7 +25,11 @@ final public class ConfigUtil { return a.equals(b); } - static String renderJsonString(String s) { + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. + */ + public static String renderJsonString(String s) { StringBuilder sb = new StringBuilder(); sb.append('"'); for (int i = 0; i < s.length(); ++i) { @@ -146,4 +152,34 @@ final public class ConfigUtil { return new File(url.getPath()); } } + + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. You can use the version in ConfigUtil instead. + */ + public static String joinPath(String... elements) { + return (new Path(elements)).render(); + } + + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. You can use the version in ConfigUtil instead. + */ + public static String joinPath(List elements) { + return joinPath(elements.toArray(new String[0])); + } + + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. You can use the version in ConfigUtil instead. + */ + public static List splitPath(String path) { + Path p = Path.newPath(path); + List elements = new ArrayList(); + while (p != null) { + elements.add(p.first()); + p = p.remainder(); + } + return elements; + } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java index dd8a5fa3b0..0d1bc97920 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java @@ -32,6 +32,6 @@ final class ConfigString extends AbstractConfigValue { @Override protected void render(StringBuilder sb, int indent, boolean formatted) { - sb.append(ConfigUtil.renderJsonString(value)); + sb.append(ConfigImplUtil.renderJsonString(value)); } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java index 8f1b43571c..9a8590bade 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java @@ -266,7 +266,7 @@ final class ConfigSubstitution extends AbstractConfigValue implements if (p instanceof SubstitutionExpression) { sb.append(p.toString()); } else { - sb.append(ConfigUtil.renderJsonString((String) p)); + sb.append(ConfigImplUtil.renderJsonString((String) p)); } } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java b/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java index e5b67540de..62b8ee575a 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java @@ -6,6 +6,7 @@ package com.typesafe.config.impl; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FilterReader; import java.io.IOException; import java.io.InputStream; @@ -261,6 +262,34 @@ public abstract class Parseable implements ConfigParseable { return new File(parent, filename); } + // this is a parseable that doesn't exist and just throws when you try to + // parse it + private final static class ParseableNotFound extends Parseable { + final private String what; + final private String message; + + ParseableNotFound(String what, String message, ConfigParseOptions options) { + this.what = what; + this.message = message; + postConstruct(options); + } + + @Override + protected Reader reader() throws IOException { + throw new FileNotFoundException(message); + } + + @Override + protected ConfigOrigin createOrigin() { + return SimpleConfigOrigin.newSimple(what); + } + } + + public static Parseable newNotFound(String whatNotFound, String message, + ConfigParseOptions options) { + return new ParseableNotFound(whatNotFound, message, options); + } + private final static class ParseableReader extends Parseable { final private Reader reader; @@ -355,7 +384,7 @@ public abstract class Parseable implements ConfigParseable { // we want file: URLs and files to always behave the same, so switch // to a file if it's a file: URL if (input.getProtocol().equals("file")) { - return newFile(ConfigUtil.urlToFile(input), options); + return newFile(ConfigImplUtil.urlToFile(input), options); } else { return new ParseableURL(input, options); } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java b/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java index 8c1434b566..6f0de1211c 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java @@ -41,6 +41,10 @@ final class Parser { final private ConfigSyntax flavor; final private ConfigOrigin baseOrigin; final private LinkedList pathStack; + // this is the number of "equals" we are inside, + // used to modify the error message to reflect that + // someone may think this is .properties format. + int equalsCount; ParseContext(ConfigSyntax flavor, ConfigOrigin origin, Iterator tokens, ConfigIncluder includer, @@ -53,6 +57,7 @@ final class Parser { this.includer = includer; this.includeContext = includeContext; this.pathStack = new LinkedList(); + this.equalsCount = 0; } private Token nextToken() { @@ -63,12 +68,25 @@ final class Parser { t = buffer.pop(); } + if (Tokens.isProblem(t)) { + ConfigOrigin origin = t.origin(); + String message = Tokens.getProblemMessage(t); + Throwable cause = Tokens.getProblemCause(t); + boolean suggestQuotes = Tokens.getProblemSuggestQuotes(t); + if (suggestQuotes) { + message = addQuoteSuggestion(t.toString(), message); + } else { + message = addKeyName(message); + } + throw new ConfigException.Parse(origin, message, cause); + } + if (flavor == ConfigSyntax.JSON) { if (Tokens.isUnquotedText(t)) { - throw parseError("Token not allowed in valid JSON: '" - + Tokens.getUnquotedText(t) + "'"); + throw parseError(addKeyName("Token not allowed in valid JSON: '" + + Tokens.getUnquotedText(t) + "'")); } else if (Tokens.isSubstitution(t)) { - throw parseError("Substitutions (${} syntax) not allowed in JSON"); + throw parseError(addKeyName("Substitutions (${} syntax) not allowed in JSON")); } } @@ -84,7 +102,7 @@ final class Parser { while (Tokens.isNewline(t)) { // line number tokens have the line that was _ended_ by the // newline, so we have to add one. - lineNumber = Tokens.getLineNumber(t) + 1; + lineNumber = t.lineNumber() + 1; t = nextToken(); } return t; @@ -111,7 +129,7 @@ final class Parser { while (true) { if (Tokens.isNewline(t)) { // newline number is the line just ended, so add one - lineNumber = Tokens.getLineNumber(t) + 1; + lineNumber = t.lineNumber() + 1; sawSeparatorOrNewline = true; // we want to continue to also eat // a comma if there is one. @@ -172,11 +190,11 @@ final class Parser { } else if (Tokens.isUnquotedText(valueToken)) { String text = Tokens.getUnquotedText(valueToken); if (firstOrigin == null) - firstOrigin = Tokens.getUnquotedTextOrigin(valueToken); + firstOrigin = valueToken.origin(); sb.append(text); } else if (Tokens.isSubstitution(valueToken)) { if (firstOrigin == null) - firstOrigin = Tokens.getSubstitutionOrigin(valueToken); + firstOrigin = valueToken.origin(); if (sb.length() > 0) { // save string so far @@ -186,8 +204,7 @@ final class Parser { // now save substitution List expression = Tokens .getSubstitutionPathExpression(valueToken); - Path path = parsePathExpression(expression.iterator(), - Tokens.getSubstitutionOrigin(valueToken)); + Path path = parsePathExpression(expression.iterator(), valueToken.origin()); boolean optional = Tokens.getSubstitutionOptional(valueToken); minimized.add(new SubstitutionExpression(path, optional)); @@ -233,6 +250,65 @@ final class Parser { return new ConfigException.Parse(lineOrigin(), message, cause); } + + private String previousFieldName(Path lastPath) { + if (lastPath != null) { + return lastPath.render(); + } else if (pathStack.isEmpty()) + return null; + else + return pathStack.peek().render(); + } + + private String previousFieldName() { + return previousFieldName(null); + } + + private String addKeyName(String message) { + String previousFieldName = previousFieldName(); + if (previousFieldName != null) { + return "in value for key '" + previousFieldName + "': " + message; + } else { + return message; + } + } + + private String addQuoteSuggestion(String badToken, String message) { + return addQuoteSuggestion(null, equalsCount > 0, badToken, message); + } + + private String addQuoteSuggestion(Path lastPath, boolean insideEquals, String badToken, + String message) { + String previousFieldName = previousFieldName(lastPath); + + String part; + if (badToken.equals(Tokens.END.toString())) { + // EOF requires special handling for the error to make sense. + if (previousFieldName != null) + part = message + " (if you intended '" + previousFieldName + + "' to be part of a value, instead of a key, " + + "try adding double quotes around the whole value"; + else + return message; + } else { + if (previousFieldName != null) { + part = message + " (if you intended " + badToken + + " to be part of the value for '" + previousFieldName + "', " + + "try enclosing the value in double quotes"; + } else { + part = message + " (if you intended " + badToken + + " to be part of a key or string value, " + + "try enclosing the key or value in double quotes"; + } + } + + if (insideEquals) + return part + + ", or you may be able to rename the file .properties rather than .conf)"; + else + return part + ")"; + } + private AbstractConfigValue parseValue(Token token) { if (Tokens.isValue(token)) { return Tokens.getValue(token); @@ -241,8 +317,8 @@ final class Parser { } else if (token == Tokens.OPEN_SQUARE) { return parseArray(); } else { - throw parseError("Expecting a value but got wrong token: " - + token); + throw parseError(addQuoteSuggestion(token.toString(), + "Expecting a value but got wrong token: " + token)); } } @@ -283,8 +359,8 @@ final class Parser { String key = (String) Tokens.getValue(token).unwrapped(); return Path.newKey(key); } else { - throw parseError("Expecting close brace } or a field name, got " - + token); + throw parseError(addKeyName("Expecting close brace } or a field name here, got " + + token)); } } else { List expression = new ArrayList(); @@ -293,6 +369,12 @@ final class Parser { expression.add(t); t = nextToken(); // note: don't cross a newline } + + if (expression.isEmpty()) { + throw parseError(addKeyName("expecting a close brace or a field name here, got " + + t)); + } + putBack(t); // put back the token we ended with return parsePathExpression(expression.iterator(), lineOrigin()); } @@ -311,7 +393,7 @@ final class Parser { for (int i = 0; i < s.length(); ++i) { char c = s.charAt(i); - if (!ConfigUtil.isWhitespace(c)) + if (!ConfigImplUtil.isWhitespace(c)) return false; } return true; @@ -362,13 +444,18 @@ final class Parser { Map values = new HashMap(); ConfigOrigin objectOrigin = lineOrigin(); boolean afterComma = false; + Path lastPath = null; + boolean lastInsideEquals = false; + while (true) { Token t = nextTokenIgnoringNewline(); if (t == Tokens.CLOSE_CURLY) { if (flavor == ConfigSyntax.JSON && afterComma) { - throw parseError("expecting a field name after comma, got a close brace }"); + throw parseError(addQuoteSuggestion(t.toString(), + "expecting a field name after a comma, got a close brace } instead")); } else if (!hadOpenCurly) { - throw parseError("unbalanced close brace '}' with no open brace"); + throw parseError(addQuoteSuggestion(t.toString(), + "unbalanced close brace '}' with no open brace")); } break; } else if (t == Tokens.END && !hadOpenCurly) { @@ -381,6 +468,7 @@ final class Parser { } else { Path path = parseKey(t); Token afterKey = nextTokenIgnoringNewline(); + boolean insideEquals = false; // path must be on-stack while we parse the value pathStack.push(path); @@ -394,8 +482,14 @@ final class Parser { newValue = parseObject(true); } else { if (!isKeyValueSeparatorToken(afterKey)) { - throw parseError("Key may not be followed by token: " - + afterKey); + throw parseError(addQuoteSuggestion(afterKey.toString(), + "Key '" + path.render() + "' may not be followed by token: " + + afterKey)); + } + + if (afterKey == Tokens.EQUALS) { + insideEquals = true; + equalsCount += 1; } consolidateValueTokens(); @@ -403,7 +497,11 @@ final class Parser { newValue = parseValue(valueToken); } - pathStack.pop(); + lastPath = pathStack.pop(); + if (insideEquals) { + equalsCount -= 1; + } + lastInsideEquals = insideEquals; String key = path.first(); Path remaining = path.remainder(); @@ -451,25 +549,25 @@ final class Parser { t = nextTokenIgnoringNewline(); if (t == Tokens.CLOSE_CURLY) { if (!hadOpenCurly) { - throw parseError("unbalanced close brace '}' with no open brace"); + throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals, + t.toString(), "unbalanced close brace '}' with no open brace")); } break; } else if (hadOpenCurly) { - throw parseError("Expecting close brace } or a comma, got " - + t); + throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals, + t.toString(), "Expecting close brace } or a comma, got " + t)); } else { if (t == Tokens.END) { putBack(t); break; } else { - throw parseError("Expecting end of input or a comma, got " - + t); + throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals, + t.toString(), "Expecting end of input or a comma, got " + t)); } } } } - return new SimpleConfigObject(objectOrigin, - values); + return new SimpleConfigObject(objectOrigin, values); } private SimpleConfigList parseArray() { @@ -492,8 +590,11 @@ final class Parser { } else if (t == Tokens.OPEN_SQUARE) { values.add(parseArray()); } else { - throw parseError("List should have ] or a first element after the open [, instead had token: " - + t); + throw parseError(addKeyName("List should have ] or a first element after the open [, instead had token: " + + t + + " (if you want " + + t + + " to be part of a string value, then double-quote it)")); } // now remaining elements @@ -506,8 +607,11 @@ final class Parser { if (t == Tokens.CLOSE_SQUARE) { return new SimpleConfigList(arrayOrigin, values); } else { - throw parseError("List should have ended with ] or had a comma, instead had token: " - + t); + throw parseError(addKeyName("List should have ended with ] or had a comma, instead had token: " + + t + + " (if you want " + + t + + " to be part of a string value, then double-quote it)")); } } @@ -526,8 +630,11 @@ final class Parser { // we allow one trailing comma putBack(t); } else { - throw parseError("List should have had new element after a comma, instead had token: " - + t); + throw parseError(addKeyName("List should have had new element after a comma, instead had token: " + + t + + " (if you want the comma or " + + t + + " to be part of a string value, then double-quote it)")); } } } @@ -659,9 +766,12 @@ final class Parser { } else if (Tokens.isUnquotedText(t)) { text = Tokens.getUnquotedText(t); } else { - throw new ConfigException.BadPath(origin, originalText, + throw new ConfigException.BadPath( + origin, + originalText, "Token not allowed in path expression: " - + t); + + t + + " (you can double-quote this token if you really want it here)"); } addPathText(buf, false, text); @@ -728,7 +838,7 @@ final class Parser { // do something much faster than the full parser if // we just have something like "foo" or "foo.bar" private static Path speculativeFastParsePath(String path) { - String s = ConfigUtil.unicodeTrim(path); + String s = ConfigImplUtil.unicodeTrim(path); if (s.isEmpty()) return null; if (hasUnsafeChars(s)) diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Path.java b/akka-actor/src/main/java/com/typesafe/config/impl/Path.java index f19552c890..193d930002 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Path.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Path.java @@ -125,7 +125,7 @@ final class Path { if (other instanceof Path) { Path that = (Path) other; return this.first.equals(that.first) - && ConfigUtil.equalsHandlingNull(this.remainder, + && ConfigImplUtil.equalsHandlingNull(this.remainder, that.remainder); } else { return false; @@ -167,7 +167,7 @@ final class Path { private void appendToStringBuilder(StringBuilder sb) { if (hasFunkyChars(first) || first.isEmpty()) - sb.append(ConfigUtil.renderJsonString(first)); + sb.append(ConfigImplUtil.renderJsonString(first)); else sb.append(first); if (remainder != null) { diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java index 127a98a05b..17979ba6cc 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -3,10 +3,13 @@ */ package com.typesafe.config.impl; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import com.typesafe.config.Config; @@ -20,12 +23,10 @@ import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueType; /** - * One thing to keep in mind in the future: if any Collection-like APIs are - * added here, including iterators or size() or anything, then we'd have to - * grapple with whether ConfigNull values are "in" the Config (probably not) and - * we'd probably want to make the collection look flat - not like a tree. So the - * key-value pairs would be all the tree's leaf values, in a big flat list with - * their full paths. + * One thing to keep in mind in the future: as Collection-like APIs are added + * here, including iterators or size() or anything, they should be consistent + * with a one-level java.util.Map from paths to non-null values. Null values are + * not "in" the map. */ final class SimpleConfig implements Config, MergeableValue { @@ -73,6 +74,31 @@ final class SimpleConfig implements Config, MergeableValue { return object.isEmpty(); } + private static void findPaths(Set> entries, Path parent, + AbstractConfigObject obj) { + for (Map.Entry entry : obj.entrySet()) { + String elem = entry.getKey(); + ConfigValue v = entry.getValue(); + Path path = Path.newKey(elem); + if (parent != null) + path = path.prepend(parent); + if (v instanceof AbstractConfigObject) { + findPaths(entries, path, (AbstractConfigObject) v); + } else if (v instanceof ConfigNull) { + // nothing; nulls are conceptually not in a Config + } else { + entries.add(new AbstractMap.SimpleImmutableEntry(path.render(), v)); + } + } + } + + @Override + public Set> entrySet() { + Set> entries = new HashSet>(); + findPaths(entries, null, object); + return entries; + } + static private AbstractConfigValue find(AbstractConfigObject self, String pathExpression, ConfigValueType expected, String originalPath) { Path path = Path.newPath(pathExpression); @@ -440,10 +466,10 @@ final class SimpleConfig implements Config, MergeableValue { */ public static long parseDuration(String input, ConfigOrigin originForException, String pathForException) { - String s = ConfigUtil.unicodeTrim(input); + String s = ConfigImplUtil.unicodeTrim(input); String originalUnitString = getUnits(s); String unitString = originalUnitString; - String numberString = ConfigUtil.unicodeTrim(s.substring(0, s.length() + String numberString = ConfigImplUtil.unicodeTrim(s.substring(0, s.length() - unitString.length())); TimeUnit units = null; @@ -592,9 +618,9 @@ final class SimpleConfig implements Config, MergeableValue { */ public static long parseBytes(String input, ConfigOrigin originForException, String pathForException) { - String s = ConfigUtil.unicodeTrim(input); + String s = ConfigImplUtil.unicodeTrim(input); String unitString = getUnits(s); - String numberString = ConfigUtil.unicodeTrim(s.substring(0, + String numberString = ConfigImplUtil.unicodeTrim(s.substring(0, s.length() - unitString.length())); // this would be caught later anyway, but the error message diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java index 1ae914c0e4..01d5b6070b 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java @@ -97,7 +97,7 @@ final class SimpleConfigOrigin implements ConfigOrigin { && this.lineNumber == otherOrigin.lineNumber && this.endLineNumber == otherOrigin.endLineNumber && this.originType == otherOrigin.originType - && ConfigUtil.equalsHandlingNull(this.urlOrNull, otherOrigin.urlOrNull); + && ConfigImplUtil.equalsHandlingNull(this.urlOrNull, otherOrigin.urlOrNull); } else { return false; } @@ -227,7 +227,7 @@ final class SimpleConfigOrigin implements ConfigOrigin { } String mergedURL; - if (ConfigUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) { + if (ConfigImplUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) { mergedURL = a.urlOrNull; } else { mergedURL = null; @@ -252,7 +252,7 @@ final class SimpleConfigOrigin implements ConfigOrigin { count += 1; if (a.endLineNumber == b.endLineNumber) count += 1; - if (ConfigUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) + if (ConfigImplUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) count += 1; } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Token.java b/akka-actor/src/main/java/com/typesafe/config/impl/Token.java index 7c888c748e..afff3247d6 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Token.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Token.java @@ -3,20 +3,57 @@ */ package com.typesafe.config.impl; +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; + class Token { final private TokenType tokenType; + final private String debugString; + final private ConfigOrigin origin; - Token(TokenType tokenType) { - this.tokenType = tokenType; + Token(TokenType tokenType, ConfigOrigin origin) { + this(tokenType, origin, null); } - public TokenType tokenType() { + Token(TokenType tokenType, ConfigOrigin origin, String debugString) { + this.tokenType = tokenType; + this.origin = origin; + this.debugString = debugString; + } + + // this is used for singleton tokens like COMMA or OPEN_CURLY + static Token newWithoutOrigin(TokenType tokenType, String debugString) { + return new Token(tokenType, null, debugString); + } + + final TokenType tokenType() { return tokenType; } + // this is final because we don't always use the origin() accessor, + // and we don't because it throws if origin is null + final ConfigOrigin origin() { + // code is only supposed to call origin() on token types that are + // expected to have an origin. + if (origin == null) + throw new ConfigException.BugOrBroken( + "tried to get origin from token that doesn't have one: " + this); + return origin; + } + + final int lineNumber() { + if (origin != null) + return origin.lineNumber(); + else + return -1; + } + @Override public String toString() { - return tokenType.name(); + if (debugString != null) + return debugString; + else + return tokenType.name(); } protected boolean canEqual(Object other) { @@ -26,6 +63,7 @@ class Token { @Override public boolean equals(Object other) { if (other instanceof Token) { + // origin is deliberately left out return canEqual(other) && this.tokenType == ((Token) other).tokenType; } else { @@ -35,6 +73,7 @@ class Token { @Override public int hashCode() { + // origin is deliberately left out return tokenType.hashCode(); } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/TokenType.java b/akka-actor/src/main/java/com/typesafe/config/impl/TokenType.java index 19b6a106a9..ace12fa70b 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/TokenType.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/TokenType.java @@ -4,5 +4,18 @@ package com.typesafe.config.impl; enum TokenType { - START, END, COMMA, EQUALS, COLON, OPEN_CURLY, CLOSE_CURLY, OPEN_SQUARE, CLOSE_SQUARE, VALUE, NEWLINE, UNQUOTED_TEXT, SUBSTITUTION; + START, + END, + COMMA, + EQUALS, + COLON, + OPEN_CURLY, + CLOSE_CURLY, + OPEN_SQUARE, + CLOSE_SQUARE, + VALUE, + NEWLINE, + UNQUOTED_TEXT, + SUBSTITUTION, + PROBLEM; } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java b/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java index 4965b2a619..2aeb7184bc 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java @@ -16,6 +16,34 @@ import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigSyntax; final class Tokenizer { + // this exception should not leave this file + private static class ProblemException extends Exception { + private static final long serialVersionUID = 1L; + + final private Token problem; + + ProblemException(Token problem) { + this.problem = problem; + } + + Token problem() { + return problem; + } + } + + private static String asString(int codepoint) { + if (codepoint == '\n') + return "newline"; + else if (codepoint == '\t') + return "tab"; + else if (codepoint == -1) + return "end of file"; + else if (Character.isISOControl(codepoint)) + return String.format("control character 0x%x", codepoint); + else + return String.format("%c", codepoint); + } + /** * Tokenizes a Reader. Does not close the reader; you have to arrange to do * that after you're done with the returned iterator. @@ -85,20 +113,22 @@ final class Tokenizer { } } - final private ConfigOrigin origin; + final private SimpleConfigOrigin origin; final private Reader input; final private LinkedList buffer; private int lineNumber; + private ConfigOrigin lineOrigin; final private Queue tokens; final private WhitespaceSaver whitespaceSaver; final private boolean allowComments; TokenIterator(ConfigOrigin origin, Reader input, boolean allowComments) { - this.origin = origin; + this.origin = (SimpleConfigOrigin) origin; this.input = input; this.allowComments = allowComments; this.buffer = new LinkedList(); lineNumber = 1; + lineOrigin = this.origin.setLineNumber(lineNumber); tokens = new LinkedList(); tokens.add(Tokens.START); whitespaceSaver = new WhitespaceSaver(); @@ -131,11 +161,11 @@ final class Tokenizer { } static boolean isWhitespace(int c) { - return ConfigUtil.isWhitespace(c); + return ConfigImplUtil.isWhitespace(c); } static boolean isWhitespaceNotNewline(int c) { - return c != '\n' && ConfigUtil.isWhitespace(c); + return c != '\n' && ConfigImplUtil.isWhitespace(c); } private int slurpComment() { @@ -194,27 +224,44 @@ final class Tokenizer { } } - private ConfigException parseError(String message) { - return parseError(message, null); + private ProblemException problem(String message) { + return problem("", message, null); } - private ConfigException parseError(String message, Throwable cause) { - return parseError(lineOrigin(), message, cause); + private ProblemException problem(String what, String message) { + return problem(what, message, null); } - private static ConfigException parseError(ConfigOrigin origin, + private ProblemException problem(String what, String message, boolean suggestQuotes) { + return problem(what, message, suggestQuotes, null); + } + + private ProblemException problem(String what, String message, Throwable cause) { + return problem(lineOrigin, what, message, cause); + } + + private ProblemException problem(String what, String message, boolean suggestQuotes, + Throwable cause) { + return problem(lineOrigin, what, message, suggestQuotes, cause); + } + + private static ProblemException problem(ConfigOrigin origin, String what, String message, Throwable cause) { - return new ConfigException.Parse(origin, message, cause); + return problem(origin, what, message, false, cause); } - private static ConfigException parseError(ConfigOrigin origin, - String message) { - return parseError(origin, message, null); + private static ProblemException problem(ConfigOrigin origin, String what, String message, + boolean suggestQuotes, Throwable cause) { + if (what == null || message == null) + throw new ConfigException.BugOrBroken( + "internal error, creating bad ProblemException"); + return new ProblemException(Tokens.newProblem(origin, what, message, suggestQuotes, + cause)); } - private ConfigOrigin lineOrigin() { - return lineOrigin(origin, lineNumber); + private static ProblemException problem(ConfigOrigin origin, String message) { + return problem(origin, "", message, null); } private static ConfigOrigin lineOrigin(ConfigOrigin baseOrigin, @@ -234,7 +281,7 @@ final class Tokenizer { // that parses as JSON is treated the JSON way and otherwise // we assume it's a string and let the parser sort it out. private Token pullUnquotedText() { - ConfigOrigin origin = lineOrigin(); + ConfigOrigin origin = lineOrigin; StringBuilder sb = new StringBuilder(); int c = nextCharSkippingComments(); while (true) { @@ -273,7 +320,7 @@ final class Tokenizer { return Tokens.newUnquotedText(origin, s); } - private Token pullNumber(int firstChar) { + private Token pullNumber(int firstChar) throws ProblemException { StringBuilder sb = new StringBuilder(); sb.appendCodePoint(firstChar); boolean containedDecimalOrE = false; @@ -291,23 +338,20 @@ final class Tokenizer { try { if (containedDecimalOrE) { // force floating point representation - return Tokens.newDouble(lineOrigin(), - Double.parseDouble(s), s); + return Tokens.newDouble(lineOrigin, Double.parseDouble(s), s); } else { // this should throw if the integer is too large for Long - return Tokens.newLong(lineOrigin(), Long.parseLong(s), s); + return Tokens.newLong(lineOrigin, Long.parseLong(s), s); } } catch (NumberFormatException e) { - throw parseError("Invalid number: '" + s - + "' (if this is in a path, try quoting it with double quotes)", - e); + throw problem(s, "Invalid number: '" + s + "'", true /* suggestQuotes */, e); } } - private void pullEscapeSequence(StringBuilder sb) { + private void pullEscapeSequence(StringBuilder sb) throws ProblemException { int escaped = nextCharRaw(); if (escaped == -1) - throw parseError("End of input but backslash in string had nothing after it"); + throw problem("End of input but backslash in string had nothing after it"); switch (escaped) { case '"': @@ -340,67 +384,57 @@ final class Tokenizer { for (int i = 0; i < 4; ++i) { int c = nextCharSkippingComments(); if (c == -1) - throw parseError("End of input but expecting 4 hex digits for \\uXXXX escape"); + throw problem("End of input but expecting 4 hex digits for \\uXXXX escape"); a[i] = (char) c; } String digits = new String(a); try { sb.appendCodePoint(Integer.parseInt(digits, 16)); } catch (NumberFormatException e) { - throw parseError( - String.format( - "Malformed hex digits after \\u escape in string: '%s'", - digits), e); + throw problem(digits, String.format( + "Malformed hex digits after \\u escape in string: '%s'", digits), e); } } break; default: - throw parseError(String - .format("backslash followed by '%c', this is not a valid escape sequence", - escaped)); + throw problem( + asString(escaped), + String.format( + "backslash followed by '%s', this is not a valid escape sequence (quoted strings use JSON escaping, so use double-backslash \\\\ for literal backslash)", + asString(escaped))); } } - private ConfigException controlCharacterError(int c) { - String asString; - if (c == '\n') - asString = "newline"; - else if (c == '\t') - asString = "tab"; - else - asString = String.format("control character 0x%x", c); - return parseError("JSON does not allow unescaped " + asString - + " in quoted strings, use a backslash escape"); - } - - private Token pullQuotedString() { + private Token pullQuotedString() throws ProblemException { // the open quote has already been consumed StringBuilder sb = new StringBuilder(); int c = '\0'; // value doesn't get used do { c = nextCharRaw(); if (c == -1) - throw parseError("End of input but string quote was still open"); + throw problem("End of input but string quote was still open"); if (c == '\\') { pullEscapeSequence(sb); } else if (c == '"') { // end the loop, done! } else if (Character.isISOControl(c)) { - throw controlCharacterError(c); + throw problem(asString(c), "JSON does not allow unescaped " + asString(c) + + " in quoted strings, use a backslash escape"); } else { sb.appendCodePoint(c); } } while (c != '"'); - return Tokens.newString(lineOrigin(), sb.toString()); + return Tokens.newString(lineOrigin, sb.toString()); } - private Token pullSubstitution() { + private Token pullSubstitution() throws ProblemException { // the initial '$' has already been consumed - ConfigOrigin origin = lineOrigin(); + ConfigOrigin origin = lineOrigin; int c = nextCharSkippingComments(); if (c != '{') { - throw parseError("'$' not followed by {"); + throw problem(asString(c), "'$' not followed by {, '" + asString(c) + + "' not allowed after '$'", true /* suggestQuotes */); } boolean optional = false; @@ -425,7 +459,7 @@ final class Tokenizer { // end the loop, done! break; } else if (t == Tokens.END) { - throw parseError(origin, + throw problem(origin, "Substitution ${ was not closed with a }"); } else { Token whitespace = saver.check(t, origin, lineNumber); @@ -438,14 +472,16 @@ final class Tokenizer { return Tokens.newSubstitution(origin, optional, expression); } - private Token pullNextToken(WhitespaceSaver saver) { + private Token pullNextToken(WhitespaceSaver saver) throws ProblemException { int c = nextCharAfterWhitespace(saver); if (c == -1) { return Tokens.END; } else if (c == '\n') { // newline tokens have the just-ended line number + Token line = Tokens.newLine(lineOrigin); lineNumber += 1; - return Tokens.newLine(lineNumber - 1); + lineOrigin = origin.setLineNumber(lineNumber); + return line; } else { Token t = null; switch (c) { @@ -482,9 +518,8 @@ final class Tokenizer { if (firstNumberChars.indexOf(c) >= 0) { t = pullNumber(c); } else if (notInUnquotedText.indexOf(c) >= 0) { - throw parseError(String - .format("Character '%c' is not the start of any valid token", - c)); + throw problem(asString(c), "Reserved character '" + asString(c) + + "' is not allowed outside quotes", true /* suggestQuotes */); } else { putBack(c); t = pullUnquotedText(); @@ -508,7 +543,7 @@ final class Tokenizer { } } - private void queueNextToken() { + private void queueNextToken() throws ProblemException { Token t = pullNextToken(whitespaceSaver); Token whitespace = whitespaceSaver.check(t, origin, lineNumber); if (whitespace != null) @@ -525,7 +560,11 @@ final class Tokenizer { public Token next() { Token t = tokens.remove(); if (tokens.isEmpty() && t != Tokens.END) { - queueNextToken(); + try { + queueNextToken(); + } catch (ProblemException e) { + tokens.add(e.problem()); + } if (tokens.isEmpty()) throw new ConfigException.BugOrBroken( "bug: tokens queue should not be empty here"); diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java b/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java index f36527d738..9f7bd42e7c 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java @@ -9,13 +9,14 @@ import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigValueType; +/* FIXME the way the subclasses of Token are private with static isFoo and accessors is kind of ridiculous. */ final class Tokens { static private class Value extends Token { final private AbstractConfigValue value; Value(AbstractConfigValue value) { - super(TokenType.VALUE); + super(TokenType.VALUE, value.origin()); this.value = value; } @@ -25,10 +26,7 @@ final class Tokens { @Override public String toString() { - String s = tokenType().name() + "(" + value.valueType().name() - + ")"; - - return s + "='" + value().unwrapped() + "'"; + return "'" + value().unwrapped() + "' (" + value.valueType().name() + ")"; } @Override @@ -48,20 +46,13 @@ final class Tokens { } static private class Line extends Token { - final private int lineNumber; - - Line(int lineNumber) { - super(TokenType.NEWLINE); - this.lineNumber = lineNumber; - } - - int lineNumber() { - return lineNumber; + Line(ConfigOrigin origin) { + super(TokenType.NEWLINE, origin); } @Override public String toString() { - return "NEWLINE@" + lineNumber; + return "'\n'@" + lineNumber(); } @Override @@ -71,38 +62,31 @@ final class Tokens { @Override public boolean equals(Object other) { - return super.equals(other) - && ((Line) other).lineNumber == lineNumber; + return super.equals(other) && ((Line) other).lineNumber() == lineNumber(); } @Override public int hashCode() { - return 41 * (41 + super.hashCode()) + lineNumber; + return 41 * (41 + super.hashCode()) + lineNumber(); } } // This is not a Value, because it requires special processing static private class UnquotedText extends Token { - final private ConfigOrigin origin; final private String value; UnquotedText(ConfigOrigin origin, String s) { - super(TokenType.UNQUOTED_TEXT); - this.origin = origin; + super(TokenType.UNQUOTED_TEXT, origin); this.value = s; } - ConfigOrigin origin() { - return origin; - } - String value() { return value; } @Override public String toString() { - return tokenType().name() + "(" + value + ")"; + return "'" + value + "'"; } @Override @@ -122,23 +106,78 @@ final class Tokens { } } + static private class Problem extends Token { + final private String what; + final private String message; + final private boolean suggestQuotes; + final private Throwable cause; + + Problem(ConfigOrigin origin, String what, String message, boolean suggestQuotes, + Throwable cause) { + super(TokenType.PROBLEM, origin); + this.what = what; + this.message = message; + this.suggestQuotes = suggestQuotes; + this.cause = cause; + } + + String message() { + return message; + } + + boolean suggestQuotes() { + return suggestQuotes; + } + + Throwable cause() { + return cause; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('\''); + sb.append(what); + sb.append('\''); + return sb.toString(); + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof Problem; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && ((Problem) other).what.equals(what) + && ((Problem) other).message.equals(message) + && ((Problem) other).suggestQuotes == suggestQuotes + && ConfigImplUtil.equalsHandlingNull(((Problem) other).cause, cause); + } + + @Override + public int hashCode() { + int h = 41 * (41 + super.hashCode()); + h = 41 * (h + what.hashCode()); + h = 41 * (h + message.hashCode()); + h = 41 * (h + Boolean.valueOf(suggestQuotes).hashCode()); + if (cause != null) + h = 41 * (h + cause.hashCode()); + return h; + } + } + // This is not a Value, because it requires special processing static private class Substitution extends Token { - final private ConfigOrigin origin; final private boolean optional; final private List value; Substitution(ConfigOrigin origin, boolean optional, List expression) { - super(TokenType.SUBSTITUTION); - this.origin = origin; + super(TokenType.SUBSTITUTION, origin); this.optional = optional; this.value = expression; } - ConfigOrigin origin() { - return origin; - } - boolean optional() { return optional; } @@ -149,7 +188,11 @@ final class Tokens { @Override public String toString() { - return tokenType().name() + "(" + value.toString() + ")"; + StringBuilder sb = new StringBuilder(); + for (Token t : value) { + sb.append(t.toString()); + } + return "'${" + sb.toString() + "}'"; } @Override @@ -190,12 +233,32 @@ final class Tokens { return token instanceof Line; } - static int getLineNumber(Token token) { - if (token instanceof Line) { - return ((Line) token).lineNumber(); + static boolean isProblem(Token token) { + return token instanceof Problem; + } + + static String getProblemMessage(Token token) { + if (token instanceof Problem) { + return ((Problem) token).message(); } else { - throw new ConfigException.BugOrBroken( - "tried to get line number from non-newline " + token); + throw new ConfigException.BugOrBroken("tried to get problem message from " + token); + } + } + + static boolean getProblemSuggestQuotes(Token token) { + if (token instanceof Problem) { + return ((Problem) token).suggestQuotes(); + } else { + throw new ConfigException.BugOrBroken("tried to get problem suggestQuotes from " + + token); + } + } + + static Throwable getProblemCause(Token token) { + if (token instanceof Problem) { + return ((Problem) token).cause(); + } else { + throw new ConfigException.BugOrBroken("tried to get problem cause from " + token); } } @@ -212,15 +275,6 @@ final class Tokens { } } - static ConfigOrigin getUnquotedTextOrigin(Token token) { - if (token instanceof UnquotedText) { - return ((UnquotedText) token).origin(); - } else { - throw new ConfigException.BugOrBroken( - "tried to get unquoted text from " + token); - } - } - static boolean isSubstitution(Token token) { return token instanceof Substitution; } @@ -234,15 +288,6 @@ final class Tokens { } } - static ConfigOrigin getSubstitutionOrigin(Token token) { - if (token instanceof Substitution) { - return ((Substitution) token).origin(); - } else { - throw new ConfigException.BugOrBroken( - "tried to get substitution origin from " + token); - } - } - static boolean getSubstitutionOptional(Token token) { if (token instanceof Substitution) { return ((Substitution) token).optional(); @@ -252,18 +297,23 @@ final class Tokens { } } - final static Token START = new Token(TokenType.START); - final static Token END = new Token(TokenType.END); - final static Token COMMA = new Token(TokenType.COMMA); - final static Token EQUALS = new Token(TokenType.EQUALS); - final static Token COLON = new Token(TokenType.COLON); - final static Token OPEN_CURLY = new Token(TokenType.OPEN_CURLY); - final static Token CLOSE_CURLY = new Token(TokenType.CLOSE_CURLY); - final static Token OPEN_SQUARE = new Token(TokenType.OPEN_SQUARE); - final static Token CLOSE_SQUARE = new Token(TokenType.CLOSE_SQUARE); + final static Token START = Token.newWithoutOrigin(TokenType.START, "start of file"); + final static Token END = Token.newWithoutOrigin(TokenType.END, "end of file"); + final static Token COMMA = Token.newWithoutOrigin(TokenType.COMMA, "','"); + final static Token EQUALS = Token.newWithoutOrigin(TokenType.EQUALS, "'='"); + final static Token COLON = Token.newWithoutOrigin(TokenType.COLON, "':'"); + final static Token OPEN_CURLY = Token.newWithoutOrigin(TokenType.OPEN_CURLY, "'{'"); + final static Token CLOSE_CURLY = Token.newWithoutOrigin(TokenType.CLOSE_CURLY, "'}'"); + final static Token OPEN_SQUARE = Token.newWithoutOrigin(TokenType.OPEN_SQUARE, "'['"); + final static Token CLOSE_SQUARE = Token.newWithoutOrigin(TokenType.CLOSE_SQUARE, "']'"); - static Token newLine(int lineNumberJustEnded) { - return new Line(lineNumberJustEnded); + static Token newLine(ConfigOrigin origin) { + return new Line(origin); + } + + static Token newProblem(ConfigOrigin origin, String what, String message, + boolean suggestQuotes, Throwable cause) { + return new Problem(origin, what, message, suggestQuotes, cause); } static Token newUnquotedText(ConfigOrigin origin, String s) { diff --git a/akka-docs/general/configuration.rst b/akka-docs/general/configuration.rst index 795445387c..6f00cae81f 100644 --- a/akka-docs/general/configuration.rst +++ b/akka-docs/general/configuration.rst @@ -135,7 +135,7 @@ A custom ``application.conf`` might look like this:: Config file format ------------------ -The configuration file syntax is described in the `HOCON `_ +The configuration file syntax is described in the `HOCON `_ specification. Note that it supports three formats; conf, json, and properties. @@ -157,7 +157,7 @@ dev.conf: loglevel = "DEBUG" } -More advanced include and substitution mechanisms are explained in the `HOCON `_ +More advanced include and substitution mechanisms are explained in the `HOCON `_ specification.