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;
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 <em>keys</em> to values.
*
* <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
* 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.
*
* <p>
* 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 <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>
* <em>Do not implement {@code Config}</em>; 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 <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

View file

@ -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);
}
}

View file

@ -8,34 +8,38 @@ import java.util.Map;
/**
* Subtype of {@link ConfigValue} representing an object (dictionary, map)
* value, as in JSON's <code>{ "a" : 42 }</code> syntax.
*
*
* <p>
* {@code ConfigObject} implements {@code java.util.Map<String, ConfigValue>} 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}.
*
*
* <p>
* 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}.
*
*
* <p>
* The {@link ConfigValue#valueType} method on an object returns
* {@link ConfigValueType#OBJECT}.
*
*
* <p>
* 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}.
*
*
* <p>
* 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.
*
*
* <p>
* Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert
* between path expressions and individual path elements (keys).
*
* <p>
* 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.
*
*
* <p>
* <em>Do not implement {@code ConfigObject}</em>; it should only be implemented
* 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)
&& (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);

View file

@ -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);

View file

@ -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<String> failMessages = new ArrayList<String>();
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);
}
}
}

View file

@ -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<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
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) {
sb.append(p.toString());
} 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.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);
}

View file

@ -41,6 +41,10 @@ final class Parser {
final private ConfigSyntax flavor;
final private ConfigOrigin baseOrigin;
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,
Iterator<Token> tokens, ConfigIncluder includer,
@ -53,6 +57,7 @@ final class Parser {
this.includer = includer;
this.includeContext = includeContext;
this.pathStack = new LinkedList<Path>();
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<Token> 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<Token> expression = new ArrayList<Token>();
@ -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<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
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))

View file

@ -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) {

View file

@ -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<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,
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

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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<Integer> buffer;
private int lineNumber;
private ConfigOrigin lineOrigin;
final private Queue<Token> 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<Integer>();
lineNumber = 1;
lineOrigin = this.origin.setLineNumber(lineNumber);
tokens = new LinkedList<Token>();
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");

View file

@ -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<Token> value;
Substitution(ConfigOrigin origin, boolean optional, List<Token> 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) {

View file

@ -135,7 +135,7 @@ A custom ``application.conf`` might look like this::
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.
@ -157,7 +157,7 @@ dev.conf:
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.