Updated to latest config release from typesafehub, v0.1.8

This commit is contained in:
Patrik Nordwall 2011-12-13 15:28:14 +01:00
parent 18601fbbb8
commit 31e2cb354f
20 changed files with 700 additions and 240 deletions

View file

@ -4,6 +4,8 @@
package com.typesafe.config; package com.typesafe.config;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* An immutable map from config paths to config values. * 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 <em>keys</em> to values. * {@code ConfigObject} is a tree of nested maps from <em>keys</em> to values.
* *
* <p> * <p>
* Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert
* between path expressions and individual path elements (keys).
*
* <p>
* Another difference between {@code Config} and {@code ConfigObject} is that * Another difference between {@code Config} and {@code ConfigObject} is that
* conceptually, {@code ConfigValue}s with a {@link ConfigValue#valueType() * conceptually, {@code ConfigValue}s with a {@link ConfigValue#valueType()
* valueType()} of {@link ConfigValueType#NULL NULL} exist in a * valueType()} of {@link ConfigValueType#NULL NULL} exist in a
@ -54,10 +60,11 @@ import java.util.List;
* are performed for you though. * are performed for you though.
* *
* <p> * <p>
* If you want to iterate over the contents of a {@code Config}, you have to get * If you want to iterate over the contents of a {@code Config}, you can get its
* its {@code ConfigObject} with {@link #root()}, and then iterate over the * {@code ConfigObject} with {@link #root()}, and then iterate over the
* {@code ConfigObject}. * {@code ConfigObject} (which implements <code>java.util.Map</code>). Or, you
* * can use {@link #entrySet()} which recurses the object tree for you and builds
* up a <code>Set</code> of all path-value pairs where the value is not null.
* *
* <p> * <p>
* <em>Do not implement {@code Config}</em>; it should only be implemented by * <em>Do not implement {@code Config}</em>; it should only be implemented by
@ -256,6 +263,17 @@ public interface Config extends ConfigMergeable {
*/ */
boolean isEmpty(); 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 <code>root().entrySet()</code> 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<Map.Entry<String, ConfigValue>> entrySet();
/** /**
* *
* @param path * @param path

View file

@ -11,7 +11,7 @@ import java.util.Map;
import java.util.Properties; import java.util.Properties;
import com.typesafe.config.impl.ConfigImpl; import com.typesafe.config.impl.ConfigImpl;
import com.typesafe.config.impl.ConfigUtil; import com.typesafe.config.impl.ConfigImplUtil;
import com.typesafe.config.impl.Parseable; import com.typesafe.config.impl.Parseable;
/** /**
@ -179,7 +179,7 @@ public final class ConfigFactory {
try { try {
return DefaultConfigHolder.defaultConfig; return DefaultConfigHolder.defaultConfig;
} catch (ExceptionInInitializerError e) { } catch (ExceptionInInitializerError e) {
throw ConfigUtil.extractInitializerError(e); throw ConfigImplUtil.extractInitializerError(e);
} }
} }

View file

@ -8,34 +8,38 @@ import java.util.Map;
/** /**
* Subtype of {@link ConfigValue} representing an object (dictionary, map) * Subtype of {@link ConfigValue} representing an object (dictionary, map)
* value, as in JSON's <code>{ "a" : 42 }</code> syntax. * value, as in JSON's <code>{ "a" : 42 }</code> syntax.
* *
* <p> * <p>
* {@code ConfigObject} implements {@code java.util.Map<String, ConfigValue>} so * {@code ConfigObject} implements {@code java.util.Map<String, ConfigValue>} so
* you can use it like a regular Java map. Or call {@link #unwrapped()} to * 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 * unwrap the map to a map with plain Java values rather than
* {@code ConfigValue}. * {@code ConfigValue}.
* *
* <p> * <p>
* Like all {@link ConfigValue} subtypes, {@code ConfigObject} is immutable. * Like all {@link ConfigValue} subtypes, {@code ConfigObject} is immutable.
* This makes it threadsafe and you never have to create "defensive copies." The * This makes it threadsafe and you never have to create "defensive copies." The
* mutator methods from {@link java.util.Map} all throw * mutator methods from {@link java.util.Map} all throw
* {@link java.lang.UnsupportedOperationException}. * {@link java.lang.UnsupportedOperationException}.
* *
* <p> * <p>
* The {@link ConfigValue#valueType} method on an object returns * The {@link ConfigValue#valueType} method on an object returns
* {@link ConfigValueType#OBJECT}. * {@link ConfigValueType#OBJECT}.
* *
* <p> * <p>
* In most cases you want to use the {@link Config} interface rather than this * 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 * one. Call {@link #toConfig()} to convert a {@code ConfigObject} to a
* {@code Config}. * {@code Config}.
* *
* <p> * <p>
* The API for a {@code ConfigObject} is in terms of keys, while the API for a * 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, * {@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 tree of maps from keys to values, while a
* {@code ConfigObject} is a one-level map from paths to values. * {@code ConfigObject} is a one-level map from paths to values.
* *
* <p>
* Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert
* between path expressions and individual path elements (keys).
*
* <p> * <p>
* A {@code ConfigObject} may contain null values, which will have * A {@code ConfigObject} may contain null values, which will have
* {@link ConfigValue#valueType()} equal to {@link ConfigValueType#NULL}. If * {@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 * file (or wherever this value tree came from). If {@code get()} returns a
* {@link ConfigValue} with type {@code ConfigValueType#NULL} then the key was * {@link ConfigValue} with type {@code ConfigValueType#NULL} then the key was
* set to null explicitly in the config file. * set to null explicitly in the config file.
* *
* <p> * <p>
* <em>Do not implement {@code ConfigObject}</em>; it should only be implemented * <em>Do not implement {@code ConfigObject}</em>; it should only be implemented
* by the config library. Arbitrary implementations will not work because the * by the config library. Arbitrary implementations will not work because the

View file

@ -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<String> 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<String> splitPath(String path) {
return ConfigImplUtil.splitPath(path);
}
}

View file

@ -144,7 +144,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
return canEqual(other) return canEqual(other)
&& (this.valueType() == && (this.valueType() ==
((ConfigValue) other).valueType()) ((ConfigValue) other).valueType())
&& ConfigUtil.equalsHandlingNull(this.unwrapped(), && ConfigImplUtil.equalsHandlingNull(this.unwrapped(),
((ConfigValue) other).unwrapped()); ((ConfigValue) other).unwrapped());
} else { } else {
return false; return false;
@ -178,7 +178,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) { protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) {
if (atKey != null) { if (atKey != null) {
sb.append(ConfigUtil.renderJsonString(atKey)); sb.append(ConfigImplUtil.renderJsonString(atKey));
sb.append(" : "); sb.append(" : ");
} }
render(sb, indent, formatted); render(sb, indent, formatted);

View file

@ -189,7 +189,7 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
indent(sb, indent); indent(sb, indent);
if (atKey != null) { if (atKey != null) {
sb.append("# unmerged value " + i + " for key " sb.append("# unmerged value " + i + " for key "
+ ConfigUtil.renderJsonString(atKey) + " from "); + ConfigImplUtil.renderJsonString(atKey) + " from ");
} else { } else {
sb.append("# unmerged value " + i + " from "); sb.append("# unmerged value " + i + " from ");
} }
@ -200,7 +200,7 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
} }
if (atKey != null) { if (atKey != null) {
sb.append(ConfigUtil.renderJsonString(atKey)); sb.append(ConfigImplUtil.renderJsonString(atKey));
sb.append(" : "); sb.append(" : ");
} }
v.render(sb, indent, formatted); v.render(sb, indent, formatted);

View file

@ -40,47 +40,66 @@ public class ConfigImpl {
|| name.endsWith(".properties")) { || name.endsWith(".properties")) {
ConfigParseable p = source.nameToParseable(name); ConfigParseable p = source.nameToParseable(name);
if (p != null) { obj = p.parse(p.options().setAllowMissing(options.getAllowMissing()));
obj = p.parse(p.options().setAllowMissing(
options.getAllowMissing()));
} else {
obj = SimpleConfigObject.emptyMissing(SimpleConfigOrigin.newSimple(name));
}
} else { } else {
ConfigParseable confHandle = source.nameToParseable(name + ".conf"); ConfigParseable confHandle = source.nameToParseable(name + ".conf");
ConfigParseable jsonHandle = source.nameToParseable(name + ".json"); ConfigParseable jsonHandle = source.nameToParseable(name + ".json");
ConfigParseable propsHandle = source.nameToParseable(name ConfigParseable propsHandle = source.nameToParseable(name
+ ".properties"); + ".properties");
boolean gotSomething = false;
if (!options.getAllowMissing() && confHandle == null List<String> failMessages = new ArrayList<String>();
&& jsonHandle == null && propsHandle == null) {
throw new ConfigException.IO(SimpleConfigOrigin.newSimple(name),
"No config files {.conf,.json,.properties} found");
}
ConfigSyntax syntax = options.getSyntax(); ConfigSyntax syntax = options.getSyntax();
obj = SimpleConfigObject.empty(SimpleConfigOrigin.newSimple(name)); obj = SimpleConfigObject.empty(SimpleConfigOrigin.newSimple(name));
if (confHandle != null if (syntax == null || syntax == ConfigSyntax.CONF) {
&& (syntax == null || syntax == ConfigSyntax.CONF)) { try {
obj = confHandle.parse(confHandle.options() obj = confHandle.parse(confHandle.options().setAllowMissing(false)
.setAllowMissing(true).setSyntax(ConfigSyntax.CONF)); .setSyntax(ConfigSyntax.CONF));
gotSomething = true;
} catch (ConfigException.IO e) {
failMessages.add(e.getMessage());
}
} }
if (jsonHandle != null if (syntax == null || syntax == ConfigSyntax.JSON) {
&& (syntax == null || syntax == ConfigSyntax.JSON)) { try {
ConfigObject parsed = jsonHandle.parse(jsonHandle ConfigObject parsed = jsonHandle.parse(jsonHandle.options()
.options().setAllowMissing(true) .setAllowMissing(false).setSyntax(ConfigSyntax.JSON));
.setSyntax(ConfigSyntax.JSON)); obj = obj.withFallback(parsed);
obj = obj.withFallback(parsed); gotSomething = true;
} catch (ConfigException.IO e) {
failMessages.add(e.getMessage());
}
} }
if (propsHandle != null if (syntax == null || syntax == ConfigSyntax.PROPERTIES) {
&& (syntax == null || syntax == ConfigSyntax.PROPERTIES)) { try {
ConfigObject parsed = propsHandle.parse(propsHandle.options() ConfigObject parsed = propsHandle.parse(propsHandle.options()
.setAllowMissing(true) .setAllowMissing(false).setSyntax(ConfigSyntax.PROPERTIES));
.setSyntax(ConfigSyntax.PROPERTIES)); obj = obj.withFallback(parsed);
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() { NameSource source = new NameSource() {
@Override @Override
public ConfigParseable nameToParseable(String name) { 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 { try {
return DefaultIncluderHolder.defaultIncluder; return DefaultIncluderHolder.defaultIncluder;
} catch (ExceptionInInitializerError e) { } catch (ExceptionInInitializerError e) {
throw ConfigUtil.extractInitializerError(e); throw ConfigImplUtil.extractInitializerError(e);
} }
} }
@ -326,7 +352,7 @@ public class ConfigImpl {
try { try {
return SystemPropertiesHolder.systemProperties; return SystemPropertiesHolder.systemProperties;
} catch (ExceptionInInitializerError e) { } catch (ExceptionInInitializerError e) {
throw ConfigUtil.extractInitializerError(e); throw ConfigImplUtil.extractInitializerError(e);
} }
} }
@ -362,7 +388,7 @@ public class ConfigImpl {
try { try {
return EnvVariablesHolder.envVariables; return EnvVariablesHolder.envVariables;
} catch (ExceptionInInitializerError e) { } catch (ExceptionInInitializerError e) {
throw ConfigUtil.extractInitializerError(e); throw ConfigImplUtil.extractInitializerError(e);
} }
} }
@ -384,7 +410,7 @@ public class ConfigImpl {
try { try {
return ReferenceHolder.referenceConfig; return ReferenceHolder.referenceConfig;
} catch (ExceptionInInitializerError e) { } catch (ExceptionInInitializerError e) {
throw ConfigUtil.extractInitializerError(e); throw ConfigImplUtil.extractInitializerError(e);
} }
} }
} }

View file

@ -6,12 +6,14 @@ package com.typesafe.config.impl;
import java.io.File; import java.io.File;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigException;
/** This is public just for the "config" package to use, don't touch it */ /** 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) { static boolean equalsHandlingNull(Object a, Object b) {
if (a == null && b != null) if (a == null && b != null)
return false; return false;
@ -23,7 +25,11 @@ final public class ConfigUtil {
return a.equals(b); 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(); StringBuilder sb = new StringBuilder();
sb.append('"'); sb.append('"');
for (int i = 0; i < s.length(); ++i) { for (int i = 0; i < s.length(); ++i) {
@ -146,4 +152,34 @@ final public class ConfigUtil {
return new File(url.getPath()); 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<String> 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<String> splitPath(String path) {
Path p = Path.newPath(path);
List<String> elements = new ArrayList<String>();
while (p != null) {
elements.add(p.first());
p = p.remainder();
}
return elements;
}
} }

View file

@ -32,6 +32,6 @@ final class ConfigString extends AbstractConfigValue {
@Override @Override
protected void render(StringBuilder sb, int indent, boolean formatted) { protected void render(StringBuilder sb, int indent, boolean formatted) {
sb.append(ConfigUtil.renderJsonString(value)); sb.append(ConfigImplUtil.renderJsonString(value));
} }
} }

View file

@ -266,7 +266,7 @@ final class ConfigSubstitution extends AbstractConfigValue implements
if (p instanceof SubstitutionExpression) { if (p instanceof SubstitutionExpression) {
sb.append(p.toString()); sb.append(p.toString());
} else { } else {
sb.append(ConfigUtil.renderJsonString((String) p)); sb.append(ConfigImplUtil.renderJsonString((String) p));
} }
} }
} }

View file

@ -6,6 +6,7 @@ package com.typesafe.config.impl;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilterReader; import java.io.FilterReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -261,6 +262,34 @@ public abstract class Parseable implements ConfigParseable {
return new File(parent, filename); 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 { private final static class ParseableReader extends Parseable {
final private Reader reader; 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 // we want file: URLs and files to always behave the same, so switch
// to a file if it's a file: URL // to a file if it's a file: URL
if (input.getProtocol().equals("file")) { if (input.getProtocol().equals("file")) {
return newFile(ConfigUtil.urlToFile(input), options); return newFile(ConfigImplUtil.urlToFile(input), options);
} else { } else {
return new ParseableURL(input, options); return new ParseableURL(input, options);
} }

View file

@ -41,6 +41,10 @@ final class Parser {
final private ConfigSyntax flavor; final private ConfigSyntax flavor;
final private ConfigOrigin baseOrigin; final private ConfigOrigin baseOrigin;
final private LinkedList<Path> pathStack; final private LinkedList<Path> 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, ParseContext(ConfigSyntax flavor, ConfigOrigin origin,
Iterator<Token> tokens, ConfigIncluder includer, Iterator<Token> tokens, ConfigIncluder includer,
@ -53,6 +57,7 @@ final class Parser {
this.includer = includer; this.includer = includer;
this.includeContext = includeContext; this.includeContext = includeContext;
this.pathStack = new LinkedList<Path>(); this.pathStack = new LinkedList<Path>();
this.equalsCount = 0;
} }
private Token nextToken() { private Token nextToken() {
@ -63,12 +68,25 @@ final class Parser {
t = buffer.pop(); 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 (flavor == ConfigSyntax.JSON) {
if (Tokens.isUnquotedText(t)) { if (Tokens.isUnquotedText(t)) {
throw parseError("Token not allowed in valid JSON: '" throw parseError(addKeyName("Token not allowed in valid JSON: '"
+ Tokens.getUnquotedText(t) + "'"); + Tokens.getUnquotedText(t) + "'"));
} else if (Tokens.isSubstitution(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)) { while (Tokens.isNewline(t)) {
// line number tokens have the line that was _ended_ by the // line number tokens have the line that was _ended_ by the
// newline, so we have to add one. // newline, so we have to add one.
lineNumber = Tokens.getLineNumber(t) + 1; lineNumber = t.lineNumber() + 1;
t = nextToken(); t = nextToken();
} }
return t; return t;
@ -111,7 +129,7 @@ final class Parser {
while (true) { while (true) {
if (Tokens.isNewline(t)) { if (Tokens.isNewline(t)) {
// newline number is the line just ended, so add one // newline number is the line just ended, so add one
lineNumber = Tokens.getLineNumber(t) + 1; lineNumber = t.lineNumber() + 1;
sawSeparatorOrNewline = true; sawSeparatorOrNewline = true;
// we want to continue to also eat // we want to continue to also eat
// a comma if there is one. // a comma if there is one.
@ -172,11 +190,11 @@ final class Parser {
} else if (Tokens.isUnquotedText(valueToken)) { } else if (Tokens.isUnquotedText(valueToken)) {
String text = Tokens.getUnquotedText(valueToken); String text = Tokens.getUnquotedText(valueToken);
if (firstOrigin == null) if (firstOrigin == null)
firstOrigin = Tokens.getUnquotedTextOrigin(valueToken); firstOrigin = valueToken.origin();
sb.append(text); sb.append(text);
} else if (Tokens.isSubstitution(valueToken)) { } else if (Tokens.isSubstitution(valueToken)) {
if (firstOrigin == null) if (firstOrigin == null)
firstOrigin = Tokens.getSubstitutionOrigin(valueToken); firstOrigin = valueToken.origin();
if (sb.length() > 0) { if (sb.length() > 0) {
// save string so far // save string so far
@ -186,8 +204,7 @@ final class Parser {
// now save substitution // now save substitution
List<Token> expression = Tokens List<Token> expression = Tokens
.getSubstitutionPathExpression(valueToken); .getSubstitutionPathExpression(valueToken);
Path path = parsePathExpression(expression.iterator(), Path path = parsePathExpression(expression.iterator(), valueToken.origin());
Tokens.getSubstitutionOrigin(valueToken));
boolean optional = Tokens.getSubstitutionOptional(valueToken); boolean optional = Tokens.getSubstitutionOptional(valueToken);
minimized.add(new SubstitutionExpression(path, optional)); minimized.add(new SubstitutionExpression(path, optional));
@ -233,6 +250,65 @@ final class Parser {
return new ConfigException.Parse(lineOrigin(), message, cause); 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) { private AbstractConfigValue parseValue(Token token) {
if (Tokens.isValue(token)) { if (Tokens.isValue(token)) {
return Tokens.getValue(token); return Tokens.getValue(token);
@ -241,8 +317,8 @@ final class Parser {
} else if (token == Tokens.OPEN_SQUARE) { } else if (token == Tokens.OPEN_SQUARE) {
return parseArray(); return parseArray();
} else { } else {
throw parseError("Expecting a value but got wrong token: " throw parseError(addQuoteSuggestion(token.toString(),
+ token); "Expecting a value but got wrong token: " + token));
} }
} }
@ -283,8 +359,8 @@ final class Parser {
String key = (String) Tokens.getValue(token).unwrapped(); String key = (String) Tokens.getValue(token).unwrapped();
return Path.newKey(key); return Path.newKey(key);
} else { } else {
throw parseError("Expecting close brace } or a field name, got " throw parseError(addKeyName("Expecting close brace } or a field name here, got "
+ token); + token));
} }
} else { } else {
List<Token> expression = new ArrayList<Token>(); List<Token> expression = new ArrayList<Token>();
@ -293,6 +369,12 @@ final class Parser {
expression.add(t); expression.add(t);
t = nextToken(); // note: don't cross a newline 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 putBack(t); // put back the token we ended with
return parsePathExpression(expression.iterator(), lineOrigin()); return parsePathExpression(expression.iterator(), lineOrigin());
} }
@ -311,7 +393,7 @@ final class Parser {
for (int i = 0; i < s.length(); ++i) { for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i); char c = s.charAt(i);
if (!ConfigUtil.isWhitespace(c)) if (!ConfigImplUtil.isWhitespace(c))
return false; return false;
} }
return true; return true;
@ -362,13 +444,18 @@ final class Parser {
Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>(); Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
ConfigOrigin objectOrigin = lineOrigin(); ConfigOrigin objectOrigin = lineOrigin();
boolean afterComma = false; boolean afterComma = false;
Path lastPath = null;
boolean lastInsideEquals = false;
while (true) { while (true) {
Token t = nextTokenIgnoringNewline(); Token t = nextTokenIgnoringNewline();
if (t == Tokens.CLOSE_CURLY) { if (t == Tokens.CLOSE_CURLY) {
if (flavor == ConfigSyntax.JSON && afterComma) { 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) { } 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; break;
} else if (t == Tokens.END && !hadOpenCurly) { } else if (t == Tokens.END && !hadOpenCurly) {
@ -381,6 +468,7 @@ final class Parser {
} else { } else {
Path path = parseKey(t); Path path = parseKey(t);
Token afterKey = nextTokenIgnoringNewline(); Token afterKey = nextTokenIgnoringNewline();
boolean insideEquals = false;
// path must be on-stack while we parse the value // path must be on-stack while we parse the value
pathStack.push(path); pathStack.push(path);
@ -394,8 +482,14 @@ final class Parser {
newValue = parseObject(true); newValue = parseObject(true);
} else { } else {
if (!isKeyValueSeparatorToken(afterKey)) { if (!isKeyValueSeparatorToken(afterKey)) {
throw parseError("Key may not be followed by token: " throw parseError(addQuoteSuggestion(afterKey.toString(),
+ afterKey); "Key '" + path.render() + "' may not be followed by token: "
+ afterKey));
}
if (afterKey == Tokens.EQUALS) {
insideEquals = true;
equalsCount += 1;
} }
consolidateValueTokens(); consolidateValueTokens();
@ -403,7 +497,11 @@ final class Parser {
newValue = parseValue(valueToken); newValue = parseValue(valueToken);
} }
pathStack.pop(); lastPath = pathStack.pop();
if (insideEquals) {
equalsCount -= 1;
}
lastInsideEquals = insideEquals;
String key = path.first(); String key = path.first();
Path remaining = path.remainder(); Path remaining = path.remainder();
@ -451,25 +549,25 @@ final class Parser {
t = nextTokenIgnoringNewline(); t = nextTokenIgnoringNewline();
if (t == Tokens.CLOSE_CURLY) { if (t == Tokens.CLOSE_CURLY) {
if (!hadOpenCurly) { 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; break;
} else if (hadOpenCurly) { } else if (hadOpenCurly) {
throw parseError("Expecting close brace } or a comma, got " throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
+ t); t.toString(), "Expecting close brace } or a comma, got " + t));
} else { } else {
if (t == Tokens.END) { if (t == Tokens.END) {
putBack(t); putBack(t);
break; break;
} else { } else {
throw parseError("Expecting end of input or a comma, got " throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
+ t); t.toString(), "Expecting end of input or a comma, got " + t));
} }
} }
} }
} }
return new SimpleConfigObject(objectOrigin, return new SimpleConfigObject(objectOrigin, values);
values);
} }
private SimpleConfigList parseArray() { private SimpleConfigList parseArray() {
@ -492,8 +590,11 @@ final class Parser {
} else if (t == Tokens.OPEN_SQUARE) { } else if (t == Tokens.OPEN_SQUARE) {
values.add(parseArray()); values.add(parseArray());
} else { } else {
throw parseError("List should have ] or a first element after the open [, instead had token: " throw parseError(addKeyName("List should have ] or a first element after the open [, instead had token: "
+ t); + t
+ " (if you want "
+ t
+ " to be part of a string value, then double-quote it)"));
} }
// now remaining elements // now remaining elements
@ -506,8 +607,11 @@ final class Parser {
if (t == Tokens.CLOSE_SQUARE) { if (t == Tokens.CLOSE_SQUARE) {
return new SimpleConfigList(arrayOrigin, values); return new SimpleConfigList(arrayOrigin, values);
} else { } else {
throw parseError("List should have ended with ] or had a comma, instead had token: " throw parseError(addKeyName("List should have ended with ] or had a comma, instead had token: "
+ t); + 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 // we allow one trailing comma
putBack(t); putBack(t);
} else { } else {
throw parseError("List should have had new element after a comma, instead had token: " throw parseError(addKeyName("List should have had new element after a comma, instead had token: "
+ t); + 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)) { } else if (Tokens.isUnquotedText(t)) {
text = Tokens.getUnquotedText(t); text = Tokens.getUnquotedText(t);
} else { } else {
throw new ConfigException.BadPath(origin, originalText, throw new ConfigException.BadPath(
origin,
originalText,
"Token not allowed in path expression: " "Token not allowed in path expression: "
+ t); + t
+ " (you can double-quote this token if you really want it here)");
} }
addPathText(buf, false, text); addPathText(buf, false, text);
@ -728,7 +838,7 @@ final class Parser {
// do something much faster than the full parser if // do something much faster than the full parser if
// we just have something like "foo" or "foo.bar" // we just have something like "foo" or "foo.bar"
private static Path speculativeFastParsePath(String path) { private static Path speculativeFastParsePath(String path) {
String s = ConfigUtil.unicodeTrim(path); String s = ConfigImplUtil.unicodeTrim(path);
if (s.isEmpty()) if (s.isEmpty())
return null; return null;
if (hasUnsafeChars(s)) if (hasUnsafeChars(s))

View file

@ -125,7 +125,7 @@ final class Path {
if (other instanceof Path) { if (other instanceof Path) {
Path that = (Path) other; Path that = (Path) other;
return this.first.equals(that.first) return this.first.equals(that.first)
&& ConfigUtil.equalsHandlingNull(this.remainder, && ConfigImplUtil.equalsHandlingNull(this.remainder,
that.remainder); that.remainder);
} else { } else {
return false; return false;
@ -167,7 +167,7 @@ final class Path {
private void appendToStringBuilder(StringBuilder sb) { private void appendToStringBuilder(StringBuilder sb) {
if (hasFunkyChars(first) || first.isEmpty()) if (hasFunkyChars(first) || first.isEmpty())
sb.append(ConfigUtil.renderJsonString(first)); sb.append(ConfigImplUtil.renderJsonString(first));
else else
sb.append(first); sb.append(first);
if (remainder != null) { if (remainder != null) {

View file

@ -3,10 +3,13 @@
*/ */
package com.typesafe.config.impl; package com.typesafe.config.impl;
import java.util.AbstractMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.typesafe.config.Config; import com.typesafe.config.Config;
@ -20,12 +23,10 @@ import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType; import com.typesafe.config.ConfigValueType;
/** /**
* One thing to keep in mind in the future: if any Collection-like APIs are * One thing to keep in mind in the future: as Collection-like APIs are added
* added here, including iterators or size() or anything, then we'd have to * here, including iterators or size() or anything, they should be consistent
* grapple with whether ConfigNull values are "in" the Config (probably not) and * with a one-level java.util.Map from paths to non-null values. Null values are
* we'd probably want to make the collection look flat - not like a tree. So the * not "in" the map.
* key-value pairs would be all the tree's leaf values, in a big flat list with
* their full paths.
*/ */
final class SimpleConfig implements Config, MergeableValue { final class SimpleConfig implements Config, MergeableValue {
@ -73,6 +74,31 @@ final class SimpleConfig implements Config, MergeableValue {
return object.isEmpty(); return object.isEmpty();
} }
private static void findPaths(Set<Map.Entry<String, ConfigValue>> entries, Path parent,
AbstractConfigObject obj) {
for (Map.Entry<String, ConfigValue> 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<String, ConfigValue>(path.render(), v));
}
}
}
@Override
public Set<Map.Entry<String, ConfigValue>> entrySet() {
Set<Map.Entry<String, ConfigValue>> entries = new HashSet<Map.Entry<String, ConfigValue>>();
findPaths(entries, null, object);
return entries;
}
static private AbstractConfigValue find(AbstractConfigObject self, static private AbstractConfigValue find(AbstractConfigObject self,
String pathExpression, ConfigValueType expected, String originalPath) { String pathExpression, ConfigValueType expected, String originalPath) {
Path path = Path.newPath(pathExpression); Path path = Path.newPath(pathExpression);
@ -440,10 +466,10 @@ final class SimpleConfig implements Config, MergeableValue {
*/ */
public static long parseDuration(String input, public static long parseDuration(String input,
ConfigOrigin originForException, String pathForException) { ConfigOrigin originForException, String pathForException) {
String s = ConfigUtil.unicodeTrim(input); String s = ConfigImplUtil.unicodeTrim(input);
String originalUnitString = getUnits(s); String originalUnitString = getUnits(s);
String unitString = originalUnitString; String unitString = originalUnitString;
String numberString = ConfigUtil.unicodeTrim(s.substring(0, s.length() String numberString = ConfigImplUtil.unicodeTrim(s.substring(0, s.length()
- unitString.length())); - unitString.length()));
TimeUnit units = null; TimeUnit units = null;
@ -592,9 +618,9 @@ final class SimpleConfig implements Config, MergeableValue {
*/ */
public static long parseBytes(String input, ConfigOrigin originForException, public static long parseBytes(String input, ConfigOrigin originForException,
String pathForException) { String pathForException) {
String s = ConfigUtil.unicodeTrim(input); String s = ConfigImplUtil.unicodeTrim(input);
String unitString = getUnits(s); String unitString = getUnits(s);
String numberString = ConfigUtil.unicodeTrim(s.substring(0, String numberString = ConfigImplUtil.unicodeTrim(s.substring(0,
s.length() - unitString.length())); s.length() - unitString.length()));
// this would be caught later anyway, but the error message // this would be caught later anyway, but the error message

View file

@ -97,7 +97,7 @@ final class SimpleConfigOrigin implements ConfigOrigin {
&& this.lineNumber == otherOrigin.lineNumber && this.lineNumber == otherOrigin.lineNumber
&& this.endLineNumber == otherOrigin.endLineNumber && this.endLineNumber == otherOrigin.endLineNumber
&& this.originType == otherOrigin.originType && this.originType == otherOrigin.originType
&& ConfigUtil.equalsHandlingNull(this.urlOrNull, otherOrigin.urlOrNull); && ConfigImplUtil.equalsHandlingNull(this.urlOrNull, otherOrigin.urlOrNull);
} else { } else {
return false; return false;
} }
@ -227,7 +227,7 @@ final class SimpleConfigOrigin implements ConfigOrigin {
} }
String mergedURL; String mergedURL;
if (ConfigUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) { if (ConfigImplUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) {
mergedURL = a.urlOrNull; mergedURL = a.urlOrNull;
} else { } else {
mergedURL = null; mergedURL = null;
@ -252,7 +252,7 @@ final class SimpleConfigOrigin implements ConfigOrigin {
count += 1; count += 1;
if (a.endLineNumber == b.endLineNumber) if (a.endLineNumber == b.endLineNumber)
count += 1; count += 1;
if (ConfigUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) if (ConfigImplUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull))
count += 1; count += 1;
} }

View file

@ -3,20 +3,57 @@
*/ */
package com.typesafe.config.impl; package com.typesafe.config.impl;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
class Token { class Token {
final private TokenType tokenType; final private TokenType tokenType;
final private String debugString;
final private ConfigOrigin origin;
Token(TokenType tokenType) { Token(TokenType tokenType, ConfigOrigin origin) {
this.tokenType = tokenType; 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; 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 @Override
public String toString() { public String toString() {
return tokenType.name(); if (debugString != null)
return debugString;
else
return tokenType.name();
} }
protected boolean canEqual(Object other) { protected boolean canEqual(Object other) {
@ -26,6 +63,7 @@ class Token {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
if (other instanceof Token) { if (other instanceof Token) {
// origin is deliberately left out
return canEqual(other) return canEqual(other)
&& this.tokenType == ((Token) other).tokenType; && this.tokenType == ((Token) other).tokenType;
} else { } else {
@ -35,6 +73,7 @@ class Token {
@Override @Override
public int hashCode() { public int hashCode() {
// origin is deliberately left out
return tokenType.hashCode(); return tokenType.hashCode();
} }
} }

View file

@ -4,5 +4,18 @@
package com.typesafe.config.impl; package com.typesafe.config.impl;
enum TokenType { 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;
} }

View file

@ -16,6 +16,34 @@ import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigSyntax; import com.typesafe.config.ConfigSyntax;
final class Tokenizer { 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 * Tokenizes a Reader. Does not close the reader; you have to arrange to do
* that after you're done with the returned iterator. * 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 Reader input;
final private LinkedList<Integer> buffer; final private LinkedList<Integer> buffer;
private int lineNumber; private int lineNumber;
private ConfigOrigin lineOrigin;
final private Queue<Token> tokens; final private Queue<Token> tokens;
final private WhitespaceSaver whitespaceSaver; final private WhitespaceSaver whitespaceSaver;
final private boolean allowComments; final private boolean allowComments;
TokenIterator(ConfigOrigin origin, Reader input, boolean allowComments) { TokenIterator(ConfigOrigin origin, Reader input, boolean allowComments) {
this.origin = origin; this.origin = (SimpleConfigOrigin) origin;
this.input = input; this.input = input;
this.allowComments = allowComments; this.allowComments = allowComments;
this.buffer = new LinkedList<Integer>(); this.buffer = new LinkedList<Integer>();
lineNumber = 1; lineNumber = 1;
lineOrigin = this.origin.setLineNumber(lineNumber);
tokens = new LinkedList<Token>(); tokens = new LinkedList<Token>();
tokens.add(Tokens.START); tokens.add(Tokens.START);
whitespaceSaver = new WhitespaceSaver(); whitespaceSaver = new WhitespaceSaver();
@ -131,11 +161,11 @@ final class Tokenizer {
} }
static boolean isWhitespace(int c) { static boolean isWhitespace(int c) {
return ConfigUtil.isWhitespace(c); return ConfigImplUtil.isWhitespace(c);
} }
static boolean isWhitespaceNotNewline(int c) { static boolean isWhitespaceNotNewline(int c) {
return c != '\n' && ConfigUtil.isWhitespace(c); return c != '\n' && ConfigImplUtil.isWhitespace(c);
} }
private int slurpComment() { private int slurpComment() {
@ -194,27 +224,44 @@ final class Tokenizer {
} }
} }
private ConfigException parseError(String message) { private ProblemException problem(String message) {
return parseError(message, null); return problem("", message, null);
} }
private ConfigException parseError(String message, Throwable cause) { private ProblemException problem(String what, String message) {
return parseError(lineOrigin(), message, cause); 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, String message,
Throwable cause) { Throwable cause) {
return new ConfigException.Parse(origin, message, cause); return problem(origin, what, message, false, cause);
} }
private static ConfigException parseError(ConfigOrigin origin, private static ProblemException problem(ConfigOrigin origin, String what, String message,
String message) { boolean suggestQuotes, Throwable cause) {
return parseError(origin, message, null); 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() { private static ProblemException problem(ConfigOrigin origin, String message) {
return lineOrigin(origin, lineNumber); return problem(origin, "", message, null);
} }
private static ConfigOrigin lineOrigin(ConfigOrigin baseOrigin, private static ConfigOrigin lineOrigin(ConfigOrigin baseOrigin,
@ -234,7 +281,7 @@ final class Tokenizer {
// that parses as JSON is treated the JSON way and otherwise // that parses as JSON is treated the JSON way and otherwise
// we assume it's a string and let the parser sort it out. // we assume it's a string and let the parser sort it out.
private Token pullUnquotedText() { private Token pullUnquotedText() {
ConfigOrigin origin = lineOrigin(); ConfigOrigin origin = lineOrigin;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
int c = nextCharSkippingComments(); int c = nextCharSkippingComments();
while (true) { while (true) {
@ -273,7 +320,7 @@ final class Tokenizer {
return Tokens.newUnquotedText(origin, s); return Tokens.newUnquotedText(origin, s);
} }
private Token pullNumber(int firstChar) { private Token pullNumber(int firstChar) throws ProblemException {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.appendCodePoint(firstChar); sb.appendCodePoint(firstChar);
boolean containedDecimalOrE = false; boolean containedDecimalOrE = false;
@ -291,23 +338,20 @@ final class Tokenizer {
try { try {
if (containedDecimalOrE) { if (containedDecimalOrE) {
// force floating point representation // force floating point representation
return Tokens.newDouble(lineOrigin(), return Tokens.newDouble(lineOrigin, Double.parseDouble(s), s);
Double.parseDouble(s), s);
} else { } else {
// this should throw if the integer is too large for Long // 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) { } catch (NumberFormatException e) {
throw parseError("Invalid number: '" + s throw problem(s, "Invalid number: '" + s + "'", true /* suggestQuotes */, e);
+ "' (if this is in a path, try quoting it with double quotes)",
e);
} }
} }
private void pullEscapeSequence(StringBuilder sb) { private void pullEscapeSequence(StringBuilder sb) throws ProblemException {
int escaped = nextCharRaw(); int escaped = nextCharRaw();
if (escaped == -1) 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) { switch (escaped) {
case '"': case '"':
@ -340,67 +384,57 @@ final class Tokenizer {
for (int i = 0; i < 4; ++i) { for (int i = 0; i < 4; ++i) {
int c = nextCharSkippingComments(); int c = nextCharSkippingComments();
if (c == -1) 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; a[i] = (char) c;
} }
String digits = new String(a); String digits = new String(a);
try { try {
sb.appendCodePoint(Integer.parseInt(digits, 16)); sb.appendCodePoint(Integer.parseInt(digits, 16));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw parseError( throw problem(digits, String.format(
String.format( "Malformed hex digits after \\u escape in string: '%s'", digits), e);
"Malformed hex digits after \\u escape in string: '%s'",
digits), e);
} }
} }
break; break;
default: default:
throw parseError(String throw problem(
.format("backslash followed by '%c', this is not a valid escape sequence", asString(escaped),
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) { private Token pullQuotedString() throws ProblemException {
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() {
// the open quote has already been consumed // the open quote has already been consumed
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
int c = '\0'; // value doesn't get used int c = '\0'; // value doesn't get used
do { do {
c = nextCharRaw(); c = nextCharRaw();
if (c == -1) 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 == '\\') { if (c == '\\') {
pullEscapeSequence(sb); pullEscapeSequence(sb);
} else if (c == '"') { } else if (c == '"') {
// end the loop, done! // end the loop, done!
} else if (Character.isISOControl(c)) { } 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 { } else {
sb.appendCodePoint(c); sb.appendCodePoint(c);
} }
} while (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 // the initial '$' has already been consumed
ConfigOrigin origin = lineOrigin(); ConfigOrigin origin = lineOrigin;
int c = nextCharSkippingComments(); int c = nextCharSkippingComments();
if (c != '{') { if (c != '{') {
throw parseError("'$' not followed by {"); throw problem(asString(c), "'$' not followed by {, '" + asString(c)
+ "' not allowed after '$'", true /* suggestQuotes */);
} }
boolean optional = false; boolean optional = false;
@ -425,7 +459,7 @@ final class Tokenizer {
// end the loop, done! // end the loop, done!
break; break;
} else if (t == Tokens.END) { } else if (t == Tokens.END) {
throw parseError(origin, throw problem(origin,
"Substitution ${ was not closed with a }"); "Substitution ${ was not closed with a }");
} else { } else {
Token whitespace = saver.check(t, origin, lineNumber); Token whitespace = saver.check(t, origin, lineNumber);
@ -438,14 +472,16 @@ final class Tokenizer {
return Tokens.newSubstitution(origin, optional, expression); return Tokens.newSubstitution(origin, optional, expression);
} }
private Token pullNextToken(WhitespaceSaver saver) { private Token pullNextToken(WhitespaceSaver saver) throws ProblemException {
int c = nextCharAfterWhitespace(saver); int c = nextCharAfterWhitespace(saver);
if (c == -1) { if (c == -1) {
return Tokens.END; return Tokens.END;
} else if (c == '\n') { } else if (c == '\n') {
// newline tokens have the just-ended line number // newline tokens have the just-ended line number
Token line = Tokens.newLine(lineOrigin);
lineNumber += 1; lineNumber += 1;
return Tokens.newLine(lineNumber - 1); lineOrigin = origin.setLineNumber(lineNumber);
return line;
} else { } else {
Token t = null; Token t = null;
switch (c) { switch (c) {
@ -482,9 +518,8 @@ final class Tokenizer {
if (firstNumberChars.indexOf(c) >= 0) { if (firstNumberChars.indexOf(c) >= 0) {
t = pullNumber(c); t = pullNumber(c);
} else if (notInUnquotedText.indexOf(c) >= 0) { } else if (notInUnquotedText.indexOf(c) >= 0) {
throw parseError(String throw problem(asString(c), "Reserved character '" + asString(c)
.format("Character '%c' is not the start of any valid token", + "' is not allowed outside quotes", true /* suggestQuotes */);
c));
} else { } else {
putBack(c); putBack(c);
t = pullUnquotedText(); t = pullUnquotedText();
@ -508,7 +543,7 @@ final class Tokenizer {
} }
} }
private void queueNextToken() { private void queueNextToken() throws ProblemException {
Token t = pullNextToken(whitespaceSaver); Token t = pullNextToken(whitespaceSaver);
Token whitespace = whitespaceSaver.check(t, origin, lineNumber); Token whitespace = whitespaceSaver.check(t, origin, lineNumber);
if (whitespace != null) if (whitespace != null)
@ -525,7 +560,11 @@ final class Tokenizer {
public Token next() { public Token next() {
Token t = tokens.remove(); Token t = tokens.remove();
if (tokens.isEmpty() && t != Tokens.END) { if (tokens.isEmpty() && t != Tokens.END) {
queueNextToken(); try {
queueNextToken();
} catch (ProblemException e) {
tokens.add(e.problem());
}
if (tokens.isEmpty()) if (tokens.isEmpty())
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken(
"bug: tokens queue should not be empty here"); "bug: tokens queue should not be empty here");

View file

@ -9,13 +9,14 @@ import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValueType; 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 { final class Tokens {
static private class Value extends Token { static private class Value extends Token {
final private AbstractConfigValue value; final private AbstractConfigValue value;
Value(AbstractConfigValue value) { Value(AbstractConfigValue value) {
super(TokenType.VALUE); super(TokenType.VALUE, value.origin());
this.value = value; this.value = value;
} }
@ -25,10 +26,7 @@ final class Tokens {
@Override @Override
public String toString() { public String toString() {
String s = tokenType().name() + "(" + value.valueType().name() return "'" + value().unwrapped() + "' (" + value.valueType().name() + ")";
+ ")";
return s + "='" + value().unwrapped() + "'";
} }
@Override @Override
@ -48,20 +46,13 @@ final class Tokens {
} }
static private class Line extends Token { static private class Line extends Token {
final private int lineNumber; Line(ConfigOrigin origin) {
super(TokenType.NEWLINE, origin);
Line(int lineNumber) {
super(TokenType.NEWLINE);
this.lineNumber = lineNumber;
}
int lineNumber() {
return lineNumber;
} }
@Override @Override
public String toString() { public String toString() {
return "NEWLINE@" + lineNumber; return "'\n'@" + lineNumber();
} }
@Override @Override
@ -71,38 +62,31 @@ final class Tokens {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return super.equals(other) return super.equals(other) && ((Line) other).lineNumber() == lineNumber();
&& ((Line) other).lineNumber == lineNumber;
} }
@Override @Override
public int hashCode() { 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 // This is not a Value, because it requires special processing
static private class UnquotedText extends Token { static private class UnquotedText extends Token {
final private ConfigOrigin origin;
final private String value; final private String value;
UnquotedText(ConfigOrigin origin, String s) { UnquotedText(ConfigOrigin origin, String s) {
super(TokenType.UNQUOTED_TEXT); super(TokenType.UNQUOTED_TEXT, origin);
this.origin = origin;
this.value = s; this.value = s;
} }
ConfigOrigin origin() {
return origin;
}
String value() { String value() {
return value; return value;
} }
@Override @Override
public String toString() { public String toString() {
return tokenType().name() + "(" + value + ")"; return "'" + value + "'";
} }
@Override @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 // This is not a Value, because it requires special processing
static private class Substitution extends Token { static private class Substitution extends Token {
final private ConfigOrigin origin;
final private boolean optional; final private boolean optional;
final private List<Token> value; final private List<Token> value;
Substitution(ConfigOrigin origin, boolean optional, List<Token> expression) { Substitution(ConfigOrigin origin, boolean optional, List<Token> expression) {
super(TokenType.SUBSTITUTION); super(TokenType.SUBSTITUTION, origin);
this.origin = origin;
this.optional = optional; this.optional = optional;
this.value = expression; this.value = expression;
} }
ConfigOrigin origin() {
return origin;
}
boolean optional() { boolean optional() {
return optional; return optional;
} }
@ -149,7 +188,11 @@ final class Tokens {
@Override @Override
public String toString() { public String toString() {
return tokenType().name() + "(" + value.toString() + ")"; StringBuilder sb = new StringBuilder();
for (Token t : value) {
sb.append(t.toString());
}
return "'${" + sb.toString() + "}'";
} }
@Override @Override
@ -190,12 +233,32 @@ final class Tokens {
return token instanceof Line; return token instanceof Line;
} }
static int getLineNumber(Token token) { static boolean isProblem(Token token) {
if (token instanceof Line) { return token instanceof Problem;
return ((Line) token).lineNumber(); }
static String getProblemMessage(Token token) {
if (token instanceof Problem) {
return ((Problem) token).message();
} else { } else {
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken("tried to get problem message from " + token);
"tried to get line number from non-newline " + 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) { static boolean isSubstitution(Token token) {
return token instanceof Substitution; 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) { static boolean getSubstitutionOptional(Token token) {
if (token instanceof Substitution) { if (token instanceof Substitution) {
return ((Substitution) token).optional(); return ((Substitution) token).optional();
@ -252,18 +297,23 @@ final class Tokens {
} }
} }
final static Token START = new Token(TokenType.START); final static Token START = Token.newWithoutOrigin(TokenType.START, "start of file");
final static Token END = new Token(TokenType.END); final static Token END = Token.newWithoutOrigin(TokenType.END, "end of file");
final static Token COMMA = new Token(TokenType.COMMA); final static Token COMMA = Token.newWithoutOrigin(TokenType.COMMA, "','");
final static Token EQUALS = new Token(TokenType.EQUALS); final static Token EQUALS = Token.newWithoutOrigin(TokenType.EQUALS, "'='");
final static Token COLON = new Token(TokenType.COLON); final static Token COLON = Token.newWithoutOrigin(TokenType.COLON, "':'");
final static Token OPEN_CURLY = new Token(TokenType.OPEN_CURLY); final static Token OPEN_CURLY = Token.newWithoutOrigin(TokenType.OPEN_CURLY, "'{'");
final static Token CLOSE_CURLY = new Token(TokenType.CLOSE_CURLY); final static Token CLOSE_CURLY = Token.newWithoutOrigin(TokenType.CLOSE_CURLY, "'}'");
final static Token OPEN_SQUARE = new Token(TokenType.OPEN_SQUARE); final static Token OPEN_SQUARE = Token.newWithoutOrigin(TokenType.OPEN_SQUARE, "'['");
final static Token CLOSE_SQUARE = new Token(TokenType.CLOSE_SQUARE); final static Token CLOSE_SQUARE = Token.newWithoutOrigin(TokenType.CLOSE_SQUARE, "']'");
static Token newLine(int lineNumberJustEnded) { static Token newLine(ConfigOrigin origin) {
return new Line(lineNumberJustEnded); 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) { static Token newUnquotedText(ConfigOrigin origin, String s) {

View file

@ -135,7 +135,7 @@ A custom ``application.conf`` might look like this::
Config file format Config file format
------------------ ------------------
The configuration file syntax is described in the `HOCON <https://github.com/havocp/config/blob/master/HOCON.md>`_ The configuration file syntax is described in the `HOCON <https://github.com/typesafehub/config/blob/master/HOCON.md>`_
specification. Note that it supports three formats; conf, json, and properties. specification. Note that it supports three formats; conf, json, and properties.
@ -157,7 +157,7 @@ dev.conf:
loglevel = "DEBUG" loglevel = "DEBUG"
} }
More advanced include and substitution mechanisms are explained in the `HOCON <https://github.com/havocp/config/blob/master/HOCON.md>`_ More advanced include and substitution mechanisms are explained in the `HOCON <https://github.com/typesafehub/config/blob/master/HOCON.md>`_
specification. specification.