updated typesafe config (I should probably make this a sub-repo eventually)

This commit is contained in:
Florian Nücke 2013-12-20 20:26:58 +01:00
parent 3a87ecd6ce
commit a10d4fdce5
12 changed files with 423 additions and 99 deletions

View File

@ -6,6 +6,7 @@ package com.typesafe.config;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
/** /**
* An immutable map from config paths to config values. * An immutable map from config paths to config values.
@ -440,6 +441,8 @@ public interface Config extends ConfigMergeable {
* href="https://github.com/typesafehub/config/blob/master/HOCON.md">the * href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
* spec</a>. * spec</a>.
* *
* @deprecated As of release 1.1, replaced by {@link #getDuration(String, TimeUnit)}
*
* @param path * @param path
* path expression * path expression
* @return the duration value at the requested path, in milliseconds * @return the duration value at the requested path, in milliseconds
@ -450,13 +453,15 @@ public interface Config extends ConfigMergeable {
* @throws ConfigException.BadValue * @throws ConfigException.BadValue
* if value cannot be parsed as a number of milliseconds * if value cannot be parsed as a number of milliseconds
*/ */
Long getMilliseconds(String path); @Deprecated Long getMilliseconds(String path);
/** /**
* Get value as a duration in nanoseconds. If the value is already a number * Get value as a duration in nanoseconds. If the value is already a number
* it's taken as milliseconds and converted to nanoseconds. If it's a * it's taken as milliseconds and converted to nanoseconds. If it's a
* string, it's parsed understanding unit suffixes, as for * string, it's parsed understanding unit suffixes, as for
* {@link #getMilliseconds(String)}. * {@link #getDuration(String, TimeUnit)}.
*
* @deprecated As of release 1.1, replaced by {@link #getDuration(String, TimeUnit)}
* *
* @param path * @param path
* path expression * path expression
@ -468,7 +473,32 @@ public interface Config extends ConfigMergeable {
* @throws ConfigException.BadValue * @throws ConfigException.BadValue
* if value cannot be parsed as a number of nanoseconds * if value cannot be parsed as a number of nanoseconds
*/ */
Long getNanoseconds(String path); @Deprecated Long getNanoseconds(String path);
/**
* Gets a value as a duration in a specified
* {@link java.util.concurrent.TimeUnit TimeUnit}. If the value is already a
* number, then it's taken as milliseconds and then converted to the
* requested TimeUnit; if it's a string, it's parsed understanding units
* suffixes like "10m" or "5ns" as documented in the <a
* href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
* spec</a>.
*
* @since 1.1
*
* @param path
* path expression
* @param unit
* convert the return value to this time unit
* @return the duration value at the requested path, in the given TimeUnit
* @throws ConfigException.Missing
* if value is absent or null
* @throws ConfigException.WrongType
* if value is not convertible to Long or String
* @throws ConfigException.BadValue
* if value cannot be parsed as a number of the given TimeUnit
*/
Long getDuration(String path, TimeUnit unit);
/** /**
* Gets a list value (with any element type) as a {@link ConfigList}, which * Gets a list value (with any element type) as a {@link ConfigList}, which
@ -505,9 +535,28 @@ public interface Config extends ConfigMergeable {
List<Long> getBytesList(String path); List<Long> getBytesList(String path);
List<Long> getMillisecondsList(String path); /**
* @deprecated As of release 1.1, replaced by {@link #getDurationList(String, TimeUnit)}
*/
@Deprecated List<Long> getMillisecondsList(String path);
List<Long> getNanosecondsList(String path); /**
* @deprecated As of release 1.1, replaced by {@link #getDurationList(String, TimeUnit)}
*/
@Deprecated List<Long> getNanosecondsList(String path);
/**
* Gets a list, converting each value in the list to a duration, using the
* same rules as {@link #getDuration(String, TimeUnit)}.
*
* @since 1.1
* @param path
* a path expression
* @param unit
* time units of the returned values
* @return list of durations, in the requested units
*/
List<Long> getDurationList(String path, TimeUnit unit);
/** /**
* Clone the config with only the given path (and its children) retained; * Clone the config with only the given path (and its children) retained;

View File

@ -339,6 +339,11 @@ public abstract class ConfigException extends RuntimeException implements Serial
public String problem() { public String problem() {
return problem; return problem;
} }
@Override
public String toString() {
return "ValidationProblem(" + path + "," + origin + "," + problem + ")";
}
} }
/** /**

View File

@ -78,8 +78,10 @@ final public class ConfigImplUtil {
if (s.length() == 0) if (s.length() == 0)
return renderJsonString(s); return renderJsonString(s);
// if it starts with a hyphen or number, we have to quote
// to ensure we end up with a string and not a number
int first = s.codePointAt(0); int first = s.codePointAt(0);
if (Character.isDigit(first)) if (Character.isDigit(first) || first == '-')
return renderJsonString(s); return renderJsonString(s);
if (s.startsWith("include") || s.startsWith("true") || s.startsWith("false") if (s.startsWith("include") || s.startsWith("true") || s.startsWith("false")
@ -89,7 +91,7 @@ final public class ConfigImplUtil {
// only unquote if it's pure alphanumeric // only unquote if it's pure alphanumeric
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 (!(Character.isLetter(c) || Character.isDigit(c))) if (!(Character.isLetter(c) || Character.isDigit(c) || c == '-'))
return renderJsonString(s); return renderJsonString(s);
} }

View File

@ -18,6 +18,7 @@ import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
@ -104,6 +105,10 @@ public abstract class Parseable implements ConfigParseable {
return null; return null;
} }
ConfigSyntax contentType() {
return null;
}
ConfigParseable relativeTo(String filename) { ConfigParseable relativeTo(String filename) {
// fall back to classpath; we treat the "filename" as absolute // fall back to classpath; we treat the "filename" as absolute
// (don't add a package name in front), // (don't add a package name in front),
@ -173,7 +178,10 @@ public abstract class Parseable implements ConfigParseable {
if (finalOptions.getAllowMissing()) { if (finalOptions.getAllowMissing()) {
return SimpleConfigObject.emptyMissing(origin); return SimpleConfigObject.emptyMissing(origin);
} else { } else {
throw new ConfigException.IO(origin, e.getMessage(), e); trace("exception loading " + origin.description() + ": " + e.getClass().getName()
+ ": " + e.getMessage());
throw new ConfigException.IO(origin,
e.getClass().getName() + ": " + e.getMessage(), e);
} }
} }
} }
@ -183,8 +191,23 @@ public abstract class Parseable implements ConfigParseable {
protected AbstractConfigValue rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) protected AbstractConfigValue rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions)
throws IOException { throws IOException {
Reader reader = reader(); Reader reader = reader();
// after reader() we will have loaded the Content-Type.
ConfigSyntax contentType = contentType();
ConfigParseOptions optionsWithContentType;
if (contentType != null) {
if (ConfigImpl.traceLoadsEnabled() && finalOptions.getSyntax() != null)
trace("Overriding syntax " + finalOptions.getSyntax()
+ " with Content-Type which specified " + contentType);
optionsWithContentType = finalOptions.setSyntax(contentType);
} else {
optionsWithContentType = finalOptions;
}
try { try {
return rawParseValue(reader, origin, finalOptions); return rawParseValue(reader, origin, optionsWithContentType);
} finally { } finally {
reader.close(); reader.close();
} }
@ -237,12 +260,16 @@ public abstract class Parseable implements ConfigParseable {
} }
private static Reader readerFromStream(InputStream input) { private static Reader readerFromStream(InputStream input) {
return readerFromStream(input, "UTF-8");
}
private static Reader readerFromStream(InputStream input, String encoding) {
try { try {
// well, this is messed up. If we aren't going to close // well, this is messed up. If we aren't going to close
// the passed-in InputStream then we have no way to // the passed-in InputStream then we have no way to
// close these readers. So maybe we should not have an // close these readers. So maybe we should not have an
// InputStream version, only a Reader version. // InputStream version, only a Reader version.
Reader reader = new InputStreamReader(input, "UTF-8"); Reader reader = new InputStreamReader(input, encoding);
return new BufferedReader(reader); return new BufferedReader(reader);
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new ConfigException.BugOrBroken("Java runtime does not support UTF-8", e); throw new ConfigException.BugOrBroken("Java runtime does not support UTF-8", e);
@ -387,6 +414,7 @@ public abstract class Parseable implements ConfigParseable {
private final static class ParseableURL extends Parseable { private final static class ParseableURL extends Parseable {
final private URL input; final private URL input;
private String contentType = null;
ParseableURL(URL input, ConfigParseOptions options) { ParseableURL(URL input, ConfigParseOptions options) {
this.input = input; this.input = input;
@ -397,7 +425,22 @@ public abstract class Parseable implements ConfigParseable {
protected Reader reader() throws IOException { protected Reader reader() throws IOException {
if (ConfigImpl.traceLoadsEnabled()) if (ConfigImpl.traceLoadsEnabled())
trace("Loading config from a URL: " + input.toExternalForm()); trace("Loading config from a URL: " + input.toExternalForm());
InputStream stream = input.openStream(); URLConnection connection = input.openConnection();
connection.connect();
// save content type for later
contentType = connection.getContentType();
if (contentType != null) {
if (ConfigImpl.traceLoadsEnabled())
trace("URL sets Content-Type: '" + contentType + "'");
contentType = contentType.trim();
int semi = contentType.indexOf(';');
if (semi >= 0)
contentType = contentType.substring(0, semi);
}
InputStream stream = connection.getInputStream();
return readerFromStream(stream); return readerFromStream(stream);
} }
@ -406,6 +449,25 @@ public abstract class Parseable implements ConfigParseable {
return syntaxFromExtension(input.getPath()); return syntaxFromExtension(input.getPath());
} }
@Override
ConfigSyntax contentType() {
if (contentType != null) {
if (contentType.equals("application/json"))
return ConfigSyntax.JSON;
else if (contentType.equals("text/x-java-properties"))
return ConfigSyntax.PROPERTIES;
else if (contentType.equals("application/hocon"))
return ConfigSyntax.CONF;
else {
if (ConfigImpl.traceLoadsEnabled())
trace("'" + contentType + "' isn't a known content type");
return null;
}
} else {
return null;
}
}
@Override @Override
ConfigParseable relativeTo(String filename) { ConfigParseable relativeTo(String filename) {
URL url = relativeTo(input, filename); URL url = relativeTo(input, filename);

View File

@ -41,14 +41,26 @@ final class Parser {
TokenWithComments(Token token, List<Token> comments) { TokenWithComments(Token token, List<Token> comments) {
this.token = token; this.token = token;
this.comments = comments; this.comments = comments;
if (Tokens.isComment(token))
throw new ConfigException.BugOrBroken("tried to annotate a comment with a comment");
} }
TokenWithComments(Token token) { TokenWithComments(Token token) {
this(token, Collections.<Token> emptyList()); this(token, Collections.<Token> emptyList());
} }
TokenWithComments removeAll() {
if (comments.isEmpty())
return this;
else
return new TokenWithComments(token);
}
TokenWithComments prepend(List<Token> earlier) { TokenWithComments prepend(List<Token> earlier) {
if (this.comments.isEmpty()) { if (earlier.isEmpty()) {
return this;
} else if (this.comments.isEmpty()) {
return new TokenWithComments(token, earlier); return new TokenWithComments(token, earlier);
} else { } else {
List<Token> merged = new ArrayList<Token>(); List<Token> merged = new ArrayList<Token>();
@ -58,7 +70,18 @@ final class Parser {
} }
} }
SimpleConfigOrigin setComments(SimpleConfigOrigin origin) { TokenWithComments add(Token after) {
if (this.comments.isEmpty()) {
return new TokenWithComments(token, Collections.<Token> singletonList(after));
} else {
List<Token> merged = new ArrayList<Token>();
merged.addAll(comments);
merged.add(after);
return new TokenWithComments(token, merged);
}
}
SimpleConfigOrigin prependComments(SimpleConfigOrigin origin) {
if (comments.isEmpty()) { if (comments.isEmpty()) {
return origin; return origin;
} else { } else {
@ -66,7 +89,19 @@ final class Parser {
for (Token c : comments) { for (Token c : comments) {
newComments.add(Tokens.getCommentText(c)); newComments.add(Tokens.getCommentText(c));
} }
return origin.setComments(newComments); return origin.prependComments(newComments);
}
}
SimpleConfigOrigin appendComments(SimpleConfigOrigin origin) {
if (comments.isEmpty()) {
return origin;
} else {
List<String> newComments = new ArrayList<String>();
for (Token c : comments) {
newComments.add(Tokens.getCommentText(c));
}
return origin.appendComments(newComments);
} }
} }
@ -105,12 +140,38 @@ final class Parser {
this.equalsCount = 0; this.equalsCount = 0;
} }
static private boolean attractsTrailingComments(Token token) {
// END can't have a trailing comment; START, OPEN_CURLY, and
// OPEN_SQUARE followed by a comment should behave as if the comment
// went with the following field or element. Associating a comment
// with a newline would mess up all the logic for comment tracking,
// so don't do that either.
if (Tokens.isNewline(token) || token == Tokens.START || token == Tokens.OPEN_CURLY
|| token == Tokens.OPEN_SQUARE || token == Tokens.END)
return false;
else
return true;
}
static private boolean attractsLeadingComments(Token token) {
// a comment just before a close } generally doesn't go with the
// value before it, unless it's on the same line as that value
if (Tokens.isNewline(token) || token == Tokens.START || token == Tokens.CLOSE_CURLY
|| token == Tokens.CLOSE_SQUARE || token == Tokens.END)
return false;
else
return true;
}
private void consolidateCommentBlock(Token commentToken) { private void consolidateCommentBlock(Token commentToken) {
// a comment block "goes with" the following token // a comment block "goes with" the following token
// unless it's separated from it by a blank line. // unless it's separated from it by a blank line.
// we want to build a list of newline tokens followed // we want to build a list of newline tokens followed
// by a non-newline non-comment token; with all comments // by a non-newline non-comment token; with all comments
// associated with that final non-newline non-comment token. // associated with that final non-newline non-comment token.
// a comment AFTER a token, without an intervening newline,
// also goes with that token, but isn't handled in this method,
// instead we handle it later by peeking ahead.
List<Token> newlines = new ArrayList<Token>(); List<Token> newlines = new ArrayList<Token>();
List<Token> comments = new ArrayList<Token>(); List<Token> comments = new ArrayList<Token>();
@ -128,6 +189,11 @@ final class Parser {
comments.add(next); comments.add(next);
} else { } else {
// a non-newline non-comment token // a non-newline non-comment token
// comments before a close brace or bracket just get dumped
if (!attractsLeadingComments(next))
comments.clear();
break; break;
} }
@ -146,7 +212,7 @@ final class Parser {
} }
} }
private TokenWithComments popToken() { private TokenWithComments popTokenWithoutTrailingComment() {
if (buffer.isEmpty()) { if (buffer.isEmpty()) {
Token t = tokens.next(); Token t = tokens.next();
if (Tokens.isComment(t)) { if (Tokens.isComment(t)) {
@ -160,6 +226,35 @@ final class Parser {
} }
} }
private TokenWithComments popToken() {
TokenWithComments withPrecedingComments = popTokenWithoutTrailingComment();
// handle a comment AFTER the other token,
// but before a newline. If the next token is not
// a comment, then any comment later on the line is irrelevant
// since it would end up going with that later token, not
// this token. Comments are supposed to be processed prior
// to adding stuff to the buffer, so they can only be found
// in "tokens" not in "buffer" in theory.
if (!attractsTrailingComments(withPrecedingComments.token)) {
return withPrecedingComments;
} else if (buffer.isEmpty()) {
Token after = tokens.next();
if (Tokens.isComment(after)) {
return withPrecedingComments.add(after);
} else {
buffer.push(new TokenWithComments(after));
return withPrecedingComments;
}
} else {
// comments are supposed to get attached to a token,
// not put back in the buffer. Assert this as an invariant.
if (Tokens.isComment(buffer.peek().token))
throw new ConfigException.BugOrBroken(
"comment token should not have been in buffer: " + buffer);
return withPrecedingComments;
}
}
private TokenWithComments nextToken() { private TokenWithComments nextToken() {
TokenWithComments withComments = null; TokenWithComments withComments = null;
@ -192,6 +287,9 @@ final class Parser {
} }
private void putBack(TokenWithComments token) { private void putBack(TokenWithComments token) {
if (Tokens.isComment(token.token))
throw new ConfigException.BugOrBroken(
"comment token should have been stripped before it was available to put back");
buffer.push(token); buffer.push(token);
} }
@ -216,6 +314,19 @@ final class Parser {
return t; return t;
} }
private AbstractConfigValue addAnyCommentsAfterAnyComma(AbstractConfigValue v) {
TokenWithComments t = nextToken(); // do NOT skip newlines, we only
// want same-line comments
if (t.token == Tokens.COMMA) {
// steal the comments from after the comma
putBack(t.removeAll());
return v.withOrigin(t.appendComments(v.origin()));
} else {
putBack(t);
return v;
}
}
// In arrays and objects, comma can be omitted // In arrays and objects, comma can be omitted
// as long as there's at least one newline instead. // as long as there's at least one newline instead.
// this skips any newlines in front of a comma, // this skips any newlines in front of a comma,
@ -272,22 +383,14 @@ final class Parser {
// create only if we have value tokens // create only if we have value tokens
List<AbstractConfigValue> values = null; List<AbstractConfigValue> values = null;
TokenWithComments firstValueWithComments = null;
// ignore a newline up front // ignore a newline up front
TokenWithComments t = nextTokenIgnoringNewline(); TokenWithComments t = nextTokenIgnoringNewline();
while (true) { while (true) {
AbstractConfigValue v = null; AbstractConfigValue v = null;
if (Tokens.isValue(t.token)) { if (Tokens.isValue(t.token) || Tokens.isUnquotedText(t.token)
// if we consolidateValueTokens() multiple times then || Tokens.isSubstitution(t.token) || t.token == Tokens.OPEN_CURLY
// this value could be a concatenation, object, array, || t.token == Tokens.OPEN_SQUARE) {
// or substitution already.
v = Tokens.getValue(t.token);
} else if (Tokens.isUnquotedText(t.token)) {
v = new ConfigString(t.token.origin(), Tokens.getUnquotedText(t.token));
} else if (Tokens.isSubstitution(t.token)) {
v = new ConfigReference(t.token.origin(),
tokenToSubstitutionExpression(t.token));
} else if (t.token == Tokens.OPEN_CURLY || t.token == Tokens.OPEN_SQUARE) {
// there may be newlines _within_ the objects and arrays // there may be newlines _within_ the objects and arrays
v = parseValue(t); v = parseValue(t);
} else { } else {
@ -299,7 +402,6 @@ final class Parser {
if (values == null) { if (values == null) {
values = new ArrayList<AbstractConfigValue>(); values = new ArrayList<AbstractConfigValue>();
firstValueWithComments = t;
} }
values.add(v); values.add(v);
@ -313,11 +415,10 @@ final class Parser {
AbstractConfigValue consolidated = ConfigConcatenation.concatenate(values); AbstractConfigValue consolidated = ConfigConcatenation.concatenate(values);
putBack(new TokenWithComments(Tokens.newValue(consolidated), putBack(new TokenWithComments(Tokens.newValue(consolidated)));
firstValueWithComments.comments));
} }
private ConfigOrigin lineOrigin() { private SimpleConfigOrigin lineOrigin() {
return ((SimpleConfigOrigin) baseOrigin).setLineNumber(lineNumber); return ((SimpleConfigOrigin) baseOrigin).setLineNumber(lineNumber);
} }
@ -403,7 +504,14 @@ final class Parser {
AbstractConfigValue v; AbstractConfigValue v;
if (Tokens.isValue(t.token)) { if (Tokens.isValue(t.token)) {
// if we consolidateValueTokens() multiple times then
// this value could be a concatenation, object, array,
// or substitution already.
v = Tokens.getValue(t.token); v = Tokens.getValue(t.token);
} else if (Tokens.isUnquotedText(t.token)) {
v = new ConfigString(t.token.origin(), Tokens.getUnquotedText(t.token));
} else if (Tokens.isSubstitution(t.token)) {
v = new ConfigReference(t.token.origin(), tokenToSubstitutionExpression(t.token));
} else if (t.token == Tokens.OPEN_CURLY) { } else if (t.token == Tokens.OPEN_CURLY) {
v = parseObject(true); v = parseObject(true);
} else if (t.token == Tokens.OPEN_SQUARE) { } else if (t.token == Tokens.OPEN_SQUARE) {
@ -413,7 +521,7 @@ final class Parser {
"Expecting a value but got wrong token: " + t.token)); "Expecting a value but got wrong token: " + t.token));
} }
v = v.withOrigin(t.setComments(v.origin())); v = v.withOrigin(t.prependComments(v.origin()));
return v; return v;
} }
@ -601,7 +709,7 @@ final class Parser {
private AbstractConfigObject parseObject(boolean hadOpenCurly) { private AbstractConfigObject parseObject(boolean hadOpenCurly) {
// invoked just after the OPEN_CURLY (or START, if !hadOpenCurly) // invoked just after the OPEN_CURLY (or START, if !hadOpenCurly)
Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>(); Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
ConfigOrigin objectOrigin = lineOrigin(); SimpleConfigOrigin objectOrigin = lineOrigin();
boolean afterComma = false; boolean afterComma = false;
Path lastPath = null; Path lastPath = null;
boolean lastInsideEquals = false; boolean lastInsideEquals = false;
@ -616,6 +724,9 @@ final class Parser {
throw parseError(addQuoteSuggestion(t.toString(), throw parseError(addQuoteSuggestion(t.toString(),
"unbalanced close brace '}' with no open brace")); "unbalanced close brace '}' with no open brace"));
} }
objectOrigin = t.appendComments(objectOrigin);
break; break;
} else if (t.token == Tokens.END && !hadOpenCurly) { } else if (t.token == Tokens.END && !hadOpenCurly) {
putBack(t); putBack(t);
@ -652,8 +763,11 @@ final class Parser {
consolidateValueTokens(); consolidateValueTokens();
valueToken = nextTokenIgnoringNewline(); valueToken = nextTokenIgnoringNewline();
// put comments from separator token on the value token
valueToken = valueToken.prepend(afterKey.comments);
} }
// comments from the key token go to the value token
newValue = parseValue(valueToken.prepend(keyToken.comments)); newValue = parseValue(valueToken.prepend(keyToken.comments));
if (afterKey.token == Tokens.PLUS_EQUALS) { if (afterKey.token == Tokens.PLUS_EQUALS) {
@ -667,6 +781,8 @@ final class Parser {
newValue = ConfigConcatenation.concatenate(concat); newValue = ConfigConcatenation.concatenate(concat);
} }
newValue = addAnyCommentsAfterAnyComma(newValue);
lastPath = pathStack.pop(); lastPath = pathStack.pop();
if (insideEquals) { if (insideEquals) {
equalsCount -= 1; equalsCount -= 1;
@ -722,6 +838,9 @@ final class Parser {
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals, throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
t.toString(), "unbalanced close brace '}' with no open brace")); t.toString(), "unbalanced close brace '}' with no open brace"));
} }
objectOrigin = t.appendComments(objectOrigin);
break; break;
} else if (hadOpenCurly) { } else if (hadOpenCurly) {
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals, throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
@ -743,7 +862,7 @@ final class Parser {
private SimpleConfigList parseArray() { private SimpleConfigList parseArray() {
// invoked just after the OPEN_SQUARE // invoked just after the OPEN_SQUARE
ConfigOrigin arrayOrigin = lineOrigin(); SimpleConfigOrigin arrayOrigin = lineOrigin();
List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>(); List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();
consolidateValueTokens(); consolidateValueTokens();
@ -752,11 +871,13 @@ final class Parser {
// special-case the first element // special-case the first element
if (t.token == Tokens.CLOSE_SQUARE) { if (t.token == Tokens.CLOSE_SQUARE) {
return new SimpleConfigList(arrayOrigin, return new SimpleConfigList(t.appendComments(arrayOrigin),
Collections.<AbstractConfigValue> emptyList()); Collections.<AbstractConfigValue> emptyList());
} else if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY } else if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY
|| t.token == Tokens.OPEN_SQUARE) { || t.token == Tokens.OPEN_SQUARE) {
values.add(parseValue(t)); AbstractConfigValue v = parseValue(t);
v = addAnyCommentsAfterAnyComma(v);
values.add(v);
} else { } else {
throw parseError(addKeyName("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
@ -773,7 +894,7 @@ final class Parser {
} else { } else {
t = nextTokenIgnoringNewline(); t = nextTokenIgnoringNewline();
if (t.token == Tokens.CLOSE_SQUARE) { if (t.token == Tokens.CLOSE_SQUARE) {
return new SimpleConfigList(arrayOrigin, values); return new SimpleConfigList(t.appendComments(arrayOrigin), values);
} else { } else {
throw parseError(addKeyName("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
@ -789,7 +910,9 @@ final class Parser {
t = nextTokenIgnoringNewline(); t = nextTokenIgnoringNewline();
if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY
|| t.token == Tokens.OPEN_SQUARE) { || t.token == Tokens.OPEN_SQUARE) {
values.add(parseValue(t)); AbstractConfigValue v = parseValue(t);
v = addAnyCommentsAfterAnyComma(v);
values.add(v);
} else if (flavor != ConfigSyntax.JSON && t.token == Tokens.CLOSE_SQUARE) { } else if (flavor != ConfigSyntax.JSON && t.token == Tokens.CLOSE_SQUARE) {
// we allow one trailing comma // we allow one trailing comma
putBack(t); putBack(t);

View File

@ -14,6 +14,7 @@ import java.io.ObjectInput;
import java.io.ObjectOutput; import java.io.ObjectOutput;
import java.io.ObjectStreamException; import java.io.ObjectStreamException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -199,7 +200,12 @@ class SerializedConfigValue extends AbstractConfigValue implements Externalizabl
// not private because we use it to serialize ConfigException // not private because we use it to serialize ConfigException
static void writeOrigin(DataOutput out, SimpleConfigOrigin origin, static void writeOrigin(DataOutput out, SimpleConfigOrigin origin,
SimpleConfigOrigin baseOrigin) throws IOException { SimpleConfigOrigin baseOrigin) throws IOException {
Map<SerializedField, Object> m = origin.toFieldsDelta(baseOrigin); Map<SerializedField, Object> m;
// to serialize a null origin, we write out no fields at all
if (origin != null)
m = origin.toFieldsDelta(baseOrigin);
else
m = Collections.emptyMap();
for (Map.Entry<SerializedField, Object> e : m.entrySet()) { for (Map.Entry<SerializedField, Object> e : m.entrySet()) {
FieldOut field = new FieldOut(e.getKey()); FieldOut field = new FieldOut(e.getKey());
Object v = e.getValue(); Object v = e.getValue();

View File

@ -234,23 +234,25 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return size; return size;
} }
@Deprecated
@Override @Override
public Long getMilliseconds(String path) { public Long getMilliseconds(String path) {
long ns = getNanoseconds(path); return getDuration(path, TimeUnit.MILLISECONDS);
long ms = TimeUnit.NANOSECONDS.toMillis(ns); }
return ms;
@Deprecated
@Override
public Long getNanoseconds(String path) {
return getDuration(path, TimeUnit.NANOSECONDS);
} }
@Override @Override
public Long getNanoseconds(String path) { public Long getDuration(String path, TimeUnit unit) {
Long ns = null;
try {
ns = TimeUnit.MILLISECONDS.toNanos(getLong(path));
} catch (ConfigException.WrongType e) {
ConfigValue v = find(path, ConfigValueType.STRING); ConfigValue v = find(path, ConfigValueType.STRING);
ns = parseDuration((String) v.unwrapped(), v.origin(), path); Long result = unit.convert(
} parseDuration((String) v.unwrapped(), v.origin(), path),
return ns; TimeUnit.NANOSECONDS);
return result;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -384,36 +386,42 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
} }
@Override @Override
public List<Long> getMillisecondsList(String path) { public List<Long> getDurationList(String path, TimeUnit unit) {
List<Long> nanos = getNanosecondsList(path);
List<Long> l = new ArrayList<Long>();
for (Long n : nanos) {
l.add(TimeUnit.NANOSECONDS.toMillis(n));
}
return l;
}
@Override
public List<Long> getNanosecondsList(String path) {
List<Long> l = new ArrayList<Long>(); List<Long> l = new ArrayList<Long>();
List<? extends ConfigValue> list = getList(path); List<? extends ConfigValue> list = getList(path);
for (ConfigValue v : list) { for (ConfigValue v : list) {
if (v.valueType() == ConfigValueType.NUMBER) { if (v.valueType() == ConfigValueType.NUMBER) {
l.add(TimeUnit.MILLISECONDS.toNanos(((Number) v.unwrapped()) Long n = unit.convert(
.longValue())); ((Number) v.unwrapped()).longValue(),
TimeUnit.MILLISECONDS);
l.add(n);
} else if (v.valueType() == ConfigValueType.STRING) { } else if (v.valueType() == ConfigValueType.STRING) {
String s = (String) v.unwrapped(); String s = (String) v.unwrapped();
Long n = parseDuration(s, v.origin(), path); Long n = unit.convert(
parseDuration(s, v.origin(), path),
TimeUnit.NANOSECONDS);
l.add(n); l.add(n);
} else { } else {
throw new ConfigException.WrongType(v.origin(), path, throw new ConfigException.WrongType(v.origin(), path,
"duration string or number of nanoseconds", v "duration string or number of milliseconds",
.valueType().name()); v.valueType().name());
} }
} }
return l; return l;
} }
@Deprecated
@Override
public List<Long> getMillisecondsList(String path) {
return getDurationList(path, TimeUnit.MILLISECONDS);
}
@Deprecated
@Override
public List<Long> getNanosecondsList(String path) {
return getDurationList(path, TimeUnit.NANOSECONDS);
}
@Override @Override
public AbstractConfigObject toFallbackValue() { public AbstractConfigObject toFallbackValue() {
return object; return object;
@ -715,7 +723,8 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return false; return false;
} }
} else if (reference instanceof SimpleConfigList) { } else if (reference instanceof SimpleConfigList) {
if (value instanceof SimpleConfigList) { // objects may be convertible to lists if they have numeric keys
if (value instanceof SimpleConfigList || value instanceof SimpleConfigObject) {
return true; return true;
} else { } else {
return false; return false;
@ -759,6 +768,25 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
} }
} }
private static void checkListCompatibility(Path path, SimpleConfigList listRef,
SimpleConfigList listValue, List<ConfigException.ValidationProblem> accumulator) {
if (listRef.isEmpty() || listValue.isEmpty()) {
// can't verify type, leave alone
} else {
AbstractConfigValue refElement = listRef.get(0);
for (ConfigValue elem : listValue) {
AbstractConfigValue e = (AbstractConfigValue) elem;
if (!haveCompatibleTypes(refElement, e)) {
addProblem(accumulator, path, e.origin(), "List at '" + path.render()
+ "' contains wrong value type, expecting list of "
+ getDesc(refElement) + " but got element of type " + getDesc(e));
// don't add a problem for every last array element
break;
}
}
}
}
private static void checkValid(Path path, ConfigValue reference, AbstractConfigValue value, private static void checkValid(Path path, ConfigValue reference, AbstractConfigValue value,
List<ConfigException.ValidationProblem> accumulator) { List<ConfigException.ValidationProblem> accumulator) {
// Unmergeable is supposed to be impossible to encounter in here // Unmergeable is supposed to be impossible to encounter in here
@ -771,22 +799,16 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
} else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigList) { } else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigList) {
SimpleConfigList listRef = (SimpleConfigList) reference; SimpleConfigList listRef = (SimpleConfigList) reference;
SimpleConfigList listValue = (SimpleConfigList) value; SimpleConfigList listValue = (SimpleConfigList) value;
if (listRef.isEmpty() || listValue.isEmpty()) { checkListCompatibility(path, listRef, listValue, accumulator);
// can't verify type, leave alone } else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigObject) {
} else { // attempt conversion of indexed object to list
AbstractConfigValue refElement = listRef.get(0); SimpleConfigList listRef = (SimpleConfigList) reference;
for (ConfigValue elem : listValue) { AbstractConfigValue listValue = DefaultTransformer.transform(value,
AbstractConfigValue e = (AbstractConfigValue) elem; ConfigValueType.LIST);
if (!haveCompatibleTypes(refElement, e)) { if (listValue instanceof SimpleConfigList)
addProblem(accumulator, path, e.origin(), "List at '" + path.render() checkListCompatibility(path, listRef, (SimpleConfigList) listValue, accumulator);
+ "' contains wrong value type, expecting list of " else
+ getDesc(refElement) + " but got element of type " addWrongType(accumulator, reference, value, path);
+ getDesc(e));
// don't add a problem for every last array element
break;
}
}
}
} }
} else { } else {
addWrongType(accumulator, reference, value, path); addWrongType(accumulator, reference, value, path);

View File

@ -391,6 +391,8 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
for (String comment : v.origin().comments()) { for (String comment : v.origin().comments()) {
indent(sb, indent + 1, options); indent(sb, indent + 1, options);
sb.append("#"); sb.append("#");
if (!comment.startsWith(" "))
sb.append(' ');
sb.append(comment); sb.append(comment);
sb.append("\n"); sb.append("\n");
} }

View File

@ -93,6 +93,34 @@ final class SimpleConfigOrigin implements ConfigOrigin {
} }
} }
SimpleConfigOrigin prependComments(List<String> comments) {
if (ConfigImplUtil.equalsHandlingNull(comments, this.commentsOrNull) || comments == null) {
return this;
} else if (this.commentsOrNull == null) {
return setComments(comments);
} else {
List<String> merged = new ArrayList<String>(comments.size()
+ this.commentsOrNull.size());
merged.addAll(comments);
merged.addAll(this.commentsOrNull);
return setComments(merged);
}
}
SimpleConfigOrigin appendComments(List<String> comments) {
if (ConfigImplUtil.equalsHandlingNull(comments, this.commentsOrNull) || comments == null) {
return this;
} else if (this.commentsOrNull == null) {
return setComments(comments);
} else {
List<String> merged = new ArrayList<String>(comments.size()
+ this.commentsOrNull.size());
merged.addAll(this.commentsOrNull);
merged.addAll(comments);
return setComments(merged);
}
}
@Override @Override
public String description() { public String description() {
// not putting the URL in here for files and resources, because people // not putting the URL in here for files and resources, because people
@ -438,6 +466,10 @@ final class SimpleConfigOrigin implements ConfigOrigin {
} }
static SimpleConfigOrigin fromFields(Map<SerializedField, Object> m) throws IOException { static SimpleConfigOrigin fromFields(Map<SerializedField, Object> m) throws IOException {
// we represent a null origin as one with no fields at all
if (m.isEmpty())
return null;
String description = (String) m.get(SerializedField.ORIGIN_DESCRIPTION); String description = (String) m.get(SerializedField.ORIGIN_DESCRIPTION);
Integer lineNumber = (Integer) m.get(SerializedField.ORIGIN_LINE_NUMBER); Integer lineNumber = (Integer) m.get(SerializedField.ORIGIN_LINE_NUMBER);
Integer endLineNumber = (Integer) m.get(SerializedField.ORIGIN_END_LINE_NUMBER); Integer endLineNumber = (Integer) m.get(SerializedField.ORIGIN_END_LINE_NUMBER);

View File

@ -175,7 +175,7 @@ class SimpleIncluder implements FullIncluder {
ConfigParseable jsonHandle = source.nameToParseable(name + ".json", options); ConfigParseable jsonHandle = source.nameToParseable(name + ".json", options);
ConfigParseable propsHandle = source.nameToParseable(name + ".properties", options); ConfigParseable propsHandle = source.nameToParseable(name + ".properties", options);
boolean gotSomething = false; boolean gotSomething = false;
List<String> failMessages = new ArrayList<String>(); List<ConfigException.IO> fails = new ArrayList<ConfigException.IO>();
ConfigSyntax syntax = options.getSyntax(); ConfigSyntax syntax = options.getSyntax();
@ -186,7 +186,7 @@ class SimpleIncluder implements FullIncluder {
.setSyntax(ConfigSyntax.CONF)); .setSyntax(ConfigSyntax.CONF));
gotSomething = true; gotSomething = true;
} catch (ConfigException.IO e) { } catch (ConfigException.IO e) {
failMessages.add(e.getMessage()); fails.add(e);
} }
} }
@ -197,7 +197,7 @@ class SimpleIncluder implements FullIncluder {
obj = obj.withFallback(parsed); obj = obj.withFallback(parsed);
gotSomething = true; gotSomething = true;
} catch (ConfigException.IO e) { } catch (ConfigException.IO e) {
failMessages.add(e.getMessage()); fails.add(e);
} }
} }
@ -208,26 +208,39 @@ class SimpleIncluder implements FullIncluder {
obj = obj.withFallback(parsed); obj = obj.withFallback(parsed);
gotSomething = true; gotSomething = true;
} catch (ConfigException.IO e) { } catch (ConfigException.IO e) {
failMessages.add(e.getMessage()); fails.add(e);
} }
} }
if (!options.getAllowMissing() && !gotSomething) { if (!options.getAllowMissing() && !gotSomething) {
String failMessage; if (ConfigImpl.traceLoadsEnabled()) {
if (failMessages.isEmpty()) { // the individual exceptions should have been logged already
// with tracing enabled
ConfigImpl.trace("Did not find '" + name
+ "' with any extension (.conf, .json, .properties); "
+ "exceptions should have been logged above.");
}
if (fails.isEmpty()) {
// this should not happen // this should not happen
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken(
"should not be reached: nothing found but no exceptions thrown"); "should not be reached: nothing found but no exceptions thrown");
} else { } else {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (String msg : failMessages) { for (Throwable t : fails) {
sb.append(msg); sb.append(t.getMessage());
sb.append(", "); sb.append(", ");
} }
sb.setLength(sb.length() - 2); sb.setLength(sb.length() - 2);
failMessage = sb.toString(); throw new ConfigException.IO(SimpleConfigOrigin.newSimple(name), sb.toString(),
fails.get(0));
}
} else if (!gotSomething) {
if (ConfigImpl.traceLoadsEnabled()) {
ConfigImpl.trace("Did not find '" + name
+ "' with any extension (.conf, .json, .properties); but '" + name
+ "' is allowed to be missing. Exceptions from load attempts should have been logged above.");
} }
throw new ConfigException.IO(SimpleConfigOrigin.newSimple(name), failMessage);
} }
} }

View File

@ -355,7 +355,15 @@ final class Tokenizer {
return Tokens.newLong(lineOrigin, Long.parseLong(s), s); return Tokens.newLong(lineOrigin, Long.parseLong(s), s);
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw problem(s, "Invalid number: '" + s + "'", true /* suggestQuotes */, e); // not a number after all, see if it's an unquoted string.
for (char u : s.toCharArray()) {
if (notInUnquotedText.indexOf(u) >= 0)
throw problem(asString(u), "Reserved character '" + asString(u)
+ "' is not allowed outside quotes", true /* suggestQuotes */);
}
// no evil chars so we just decide this was a string and
// not a number.
return Tokens.newUnquotedText(lineOrigin, s);
} }
} }

View File

@ -182,8 +182,8 @@ object Settings {
val renderSettings = ConfigRenderOptions.defaults.setJson(false).setOriginComments(false) val renderSettings = ConfigRenderOptions.defaults.setJson(false).setOriginComments(false)
val out = new PrintWriter(file) val out = new PrintWriter(file)
out.write(config.root.render(renderSettings).lines. out.write(config.root.render(renderSettings).lines.
// Strip extra spaces in front and fix additional space in of comments. // Strip extra spaces in front.
map(_.stripPrefix(" ").replaceAll("^(\\s*)# ", "$1# ")). map(_.stripPrefix(" ")).
// Indent two spaces instead of four. // Indent two spaces instead of four.
map(line => """^(\s*)""".r.replaceAllIn(line, m => m.group(1).replace(" ", " "))). map(line => """^(\s*)""".r.replaceAllIn(line, m => m.group(1).replace(" ", " "))).
// Finalize the string. // Finalize the string.