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.