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.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 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
* spec</a>.
*
* @deprecated As of release 1.1, replaced by {@link #getDuration(String, TimeUnit)}
*
* @param path
* path expression
* @return the duration value at the requested path, in milliseconds
@ -450,13 +453,15 @@ public interface Config extends ConfigMergeable {
* @throws ConfigException.BadValue
* 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
* it's taken as milliseconds and converted to nanoseconds. If it's a
* 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
* path expression
@ -468,7 +473,32 @@ public interface Config extends ConfigMergeable {
* @throws ConfigException.BadValue
* 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
@ -505,9 +535,28 @@ public interface Config extends ConfigMergeable {
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;
@ -554,7 +603,7 @@ public interface Config extends ConfigMergeable {
* to the given value. Does not modify this instance (since it's immutable).
* If the path already has a value, that value is replaced. To remove a
* value, use withoutPath().
*
*
* @param path
* path to add
* @param value

View File

@ -339,6 +339,11 @@ public abstract class ConfigException extends RuntimeException implements Serial
public String 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)
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);
if (Character.isDigit(first))
if (Character.isDigit(first) || first == '-')
return renderJsonString(s);
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
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (!(Character.isLetter(c) || Character.isDigit(c)))
if (!(Character.isLetter(c) || Character.isDigit(c) || c == '-'))
return renderJsonString(s);
}

View File

@ -18,6 +18,7 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
@ -104,6 +105,10 @@ public abstract class Parseable implements ConfigParseable {
return null;
}
ConfigSyntax contentType() {
return null;
}
ConfigParseable relativeTo(String filename) {
// fall back to classpath; we treat the "filename" as absolute
// (don't add a package name in front),
@ -173,7 +178,10 @@ public abstract class Parseable implements ConfigParseable {
if (finalOptions.getAllowMissing()) {
return SimpleConfigObject.emptyMissing(origin);
} 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)
throws IOException {
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 {
return rawParseValue(reader, origin, finalOptions);
return rawParseValue(reader, origin, optionsWithContentType);
} finally {
reader.close();
}
@ -237,12 +260,16 @@ public abstract class Parseable implements ConfigParseable {
}
private static Reader readerFromStream(InputStream input) {
return readerFromStream(input, "UTF-8");
}
private static Reader readerFromStream(InputStream input, String encoding) {
try {
// well, this is messed up. If we aren't going to close
// the passed-in InputStream then we have no way to
// close these readers. So maybe we should not have an
// InputStream version, only a Reader version.
Reader reader = new InputStreamReader(input, "UTF-8");
Reader reader = new InputStreamReader(input, encoding);
return new BufferedReader(reader);
} catch (UnsupportedEncodingException 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 {
final private URL input;
private String contentType = null;
ParseableURL(URL input, ConfigParseOptions options) {
this.input = input;
@ -397,7 +425,22 @@ public abstract class Parseable implements ConfigParseable {
protected Reader reader() throws IOException {
if (ConfigImpl.traceLoadsEnabled())
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);
}
@ -406,6 +449,25 @@ public abstract class Parseable implements ConfigParseable {
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
ConfigParseable relativeTo(String filename) {
URL url = relativeTo(input, filename);

View File

@ -41,14 +41,26 @@ final class Parser {
TokenWithComments(Token token, List<Token> comments) {
this.token = token;
this.comments = comments;
if (Tokens.isComment(token))
throw new ConfigException.BugOrBroken("tried to annotate a comment with a comment");
}
TokenWithComments(Token token) {
this(token, Collections.<Token> emptyList());
}
TokenWithComments removeAll() {
if (comments.isEmpty())
return this;
else
return new TokenWithComments(token);
}
TokenWithComments prepend(List<Token> earlier) {
if (this.comments.isEmpty()) {
if (earlier.isEmpty()) {
return this;
} else if (this.comments.isEmpty()) {
return new TokenWithComments(token, earlier);
} else {
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()) {
return origin;
} else {
@ -66,7 +89,19 @@ final class Parser {
for (Token c : comments) {
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;
}
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) {
// a comment block "goes with" the following token
// unless it's separated from it by a blank line.
// we want to build a list of newline tokens followed
// by a non-newline non-comment token; with all comments
// 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> comments = new ArrayList<Token>();
@ -128,6 +189,11 @@ final class Parser {
comments.add(next);
} else {
// a non-newline non-comment token
// comments before a close brace or bracket just get dumped
if (!attractsLeadingComments(next))
comments.clear();
break;
}
@ -146,7 +212,7 @@ final class Parser {
}
}
private TokenWithComments popToken() {
private TokenWithComments popTokenWithoutTrailingComment() {
if (buffer.isEmpty()) {
Token t = tokens.next();
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() {
TokenWithComments withComments = null;
@ -192,6 +287,9 @@ final class Parser {
}
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);
}
@ -216,6 +314,19 @@ final class Parser {
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
// as long as there's at least one newline instead.
// this skips any newlines in front of a comma,
@ -272,22 +383,14 @@ final class Parser {
// create only if we have value tokens
List<AbstractConfigValue> values = null;
TokenWithComments firstValueWithComments = null;
// ignore a newline up front
TokenWithComments t = nextTokenIgnoringNewline();
while (true) {
AbstractConfigValue v = null;
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);
} 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) {
if (Tokens.isValue(t.token) || Tokens.isUnquotedText(t.token)
|| Tokens.isSubstitution(t.token) || t.token == Tokens.OPEN_CURLY
|| t.token == Tokens.OPEN_SQUARE) {
// there may be newlines _within_ the objects and arrays
v = parseValue(t);
} else {
@ -299,7 +402,6 @@ final class Parser {
if (values == null) {
values = new ArrayList<AbstractConfigValue>();
firstValueWithComments = t;
}
values.add(v);
@ -313,11 +415,10 @@ final class Parser {
AbstractConfigValue consolidated = ConfigConcatenation.concatenate(values);
putBack(new TokenWithComments(Tokens.newValue(consolidated),
firstValueWithComments.comments));
putBack(new TokenWithComments(Tokens.newValue(consolidated)));
}
private ConfigOrigin lineOrigin() {
private SimpleConfigOrigin lineOrigin() {
return ((SimpleConfigOrigin) baseOrigin).setLineNumber(lineNumber);
}
@ -403,7 +504,14 @@ final class Parser {
AbstractConfigValue v;
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);
} 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) {
v = parseObject(true);
} else if (t.token == Tokens.OPEN_SQUARE) {
@ -413,7 +521,7 @@ final class Parser {
"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;
}
@ -601,7 +709,7 @@ final class Parser {
private AbstractConfigObject parseObject(boolean hadOpenCurly) {
// invoked just after the OPEN_CURLY (or START, if !hadOpenCurly)
Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
ConfigOrigin objectOrigin = lineOrigin();
SimpleConfigOrigin objectOrigin = lineOrigin();
boolean afterComma = false;
Path lastPath = null;
boolean lastInsideEquals = false;
@ -616,6 +724,9 @@ final class Parser {
throw parseError(addQuoteSuggestion(t.toString(),
"unbalanced close brace '}' with no open brace"));
}
objectOrigin = t.appendComments(objectOrigin);
break;
} else if (t.token == Tokens.END && !hadOpenCurly) {
putBack(t);
@ -652,8 +763,11 @@ final class Parser {
consolidateValueTokens();
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));
if (afterKey.token == Tokens.PLUS_EQUALS) {
@ -667,6 +781,8 @@ final class Parser {
newValue = ConfigConcatenation.concatenate(concat);
}
newValue = addAnyCommentsAfterAnyComma(newValue);
lastPath = pathStack.pop();
if (insideEquals) {
equalsCount -= 1;
@ -722,6 +838,9 @@ final class Parser {
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
t.toString(), "unbalanced close brace '}' with no open brace"));
}
objectOrigin = t.appendComments(objectOrigin);
break;
} else if (hadOpenCurly) {
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
@ -743,7 +862,7 @@ final class Parser {
private SimpleConfigList parseArray() {
// invoked just after the OPEN_SQUARE
ConfigOrigin arrayOrigin = lineOrigin();
SimpleConfigOrigin arrayOrigin = lineOrigin();
List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();
consolidateValueTokens();
@ -752,11 +871,13 @@ final class Parser {
// special-case the first element
if (t.token == Tokens.CLOSE_SQUARE) {
return new SimpleConfigList(arrayOrigin,
return new SimpleConfigList(t.appendComments(arrayOrigin),
Collections.<AbstractConfigValue> emptyList());
} else if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY
|| t.token == Tokens.OPEN_SQUARE) {
values.add(parseValue(t));
AbstractConfigValue v = parseValue(t);
v = addAnyCommentsAfterAnyComma(v);
values.add(v);
} else {
throw parseError(addKeyName("List should have ] or a first element after the open [, instead had token: "
+ t
@ -773,7 +894,7 @@ final class Parser {
} else {
t = nextTokenIgnoringNewline();
if (t.token == Tokens.CLOSE_SQUARE) {
return new SimpleConfigList(arrayOrigin, values);
return new SimpleConfigList(t.appendComments(arrayOrigin), values);
} else {
throw parseError(addKeyName("List should have ended with ] or had a comma, instead had token: "
+ t
@ -789,7 +910,9 @@ final class Parser {
t = nextTokenIgnoringNewline();
if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY
|| 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) {
// we allow one trailing comma
putBack(t);

View File

@ -14,6 +14,7 @@ import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectStreamException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
@ -199,7 +200,12 @@ class SerializedConfigValue extends AbstractConfigValue implements Externalizabl
// not private because we use it to serialize ConfigException
static void writeOrigin(DataOutput out, SimpleConfigOrigin origin,
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()) {
FieldOut field = new FieldOut(e.getKey());
Object v = e.getValue();

View File

@ -234,23 +234,25 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return size;
}
@Deprecated
@Override
public Long getMilliseconds(String path) {
long ns = getNanoseconds(path);
long ms = TimeUnit.NANOSECONDS.toMillis(ns);
return ms;
return getDuration(path, TimeUnit.MILLISECONDS);
}
@Deprecated
@Override
public Long getNanoseconds(String path) {
return getDuration(path, TimeUnit.NANOSECONDS);
}
@Override
public Long getNanoseconds(String path) {
Long ns = null;
try {
ns = TimeUnit.MILLISECONDS.toNanos(getLong(path));
} catch (ConfigException.WrongType e) {
ConfigValue v = find(path, ConfigValueType.STRING);
ns = parseDuration((String) v.unwrapped(), v.origin(), path);
}
return ns;
public Long getDuration(String path, TimeUnit unit) {
ConfigValue v = find(path, ConfigValueType.STRING);
Long result = unit.convert(
parseDuration((String) v.unwrapped(), v.origin(), path),
TimeUnit.NANOSECONDS);
return result;
}
@SuppressWarnings("unchecked")
@ -384,36 +386,42 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
}
@Override
public List<Long> getMillisecondsList(String path) {
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) {
public List<Long> getDurationList(String path, TimeUnit unit) {
List<Long> l = new ArrayList<Long>();
List<? extends ConfigValue> list = getList(path);
for (ConfigValue v : list) {
if (v.valueType() == ConfigValueType.NUMBER) {
l.add(TimeUnit.MILLISECONDS.toNanos(((Number) v.unwrapped())
.longValue()));
Long n = unit.convert(
((Number) v.unwrapped()).longValue(),
TimeUnit.MILLISECONDS);
l.add(n);
} else if (v.valueType() == ConfigValueType.STRING) {
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);
} else {
throw new ConfigException.WrongType(v.origin(), path,
"duration string or number of nanoseconds", v
.valueType().name());
"duration string or number of milliseconds",
v.valueType().name());
}
}
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
public AbstractConfigObject toFallbackValue() {
return object;
@ -715,7 +723,8 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return false;
}
} 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;
} else {
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,
List<ConfigException.ValidationProblem> accumulator) {
// 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) {
SimpleConfigList listRef = (SimpleConfigList) reference;
SimpleConfigList listValue = (SimpleConfigList) value;
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;
}
}
}
checkListCompatibility(path, listRef, listValue, accumulator);
} else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigObject) {
// attempt conversion of indexed object to list
SimpleConfigList listRef = (SimpleConfigList) reference;
AbstractConfigValue listValue = DefaultTransformer.transform(value,
ConfigValueType.LIST);
if (listValue instanceof SimpleConfigList)
checkListCompatibility(path, listRef, (SimpleConfigList) listValue, accumulator);
else
addWrongType(accumulator, reference, value, path);
}
} else {
addWrongType(accumulator, reference, value, path);

View File

@ -390,7 +390,9 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
if (options.getComments()) {
for (String comment : v.origin().comments()) {
indent(sb, indent + 1, options);
sb.append("# ");
sb.append("#");
if (!comment.startsWith(" "))
sb.append(' ');
sb.append(comment);
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
public String description() {
// 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 {
// 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);
Integer lineNumber = (Integer) m.get(SerializedField.ORIGIN_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 propsHandle = source.nameToParseable(name + ".properties", options);
boolean gotSomething = false;
List<String> failMessages = new ArrayList<String>();
List<ConfigException.IO> fails = new ArrayList<ConfigException.IO>();
ConfigSyntax syntax = options.getSyntax();
@ -186,7 +186,7 @@ class SimpleIncluder implements FullIncluder {
.setSyntax(ConfigSyntax.CONF));
gotSomething = true;
} catch (ConfigException.IO e) {
failMessages.add(e.getMessage());
fails.add(e);
}
}
@ -197,7 +197,7 @@ class SimpleIncluder implements FullIncluder {
obj = obj.withFallback(parsed);
gotSomething = true;
} catch (ConfigException.IO e) {
failMessages.add(e.getMessage());
fails.add(e);
}
}
@ -208,26 +208,39 @@ class SimpleIncluder implements FullIncluder {
obj = obj.withFallback(parsed);
gotSomething = true;
} catch (ConfigException.IO e) {
failMessages.add(e.getMessage());
fails.add(e);
}
}
if (!options.getAllowMissing() && !gotSomething) {
String failMessage;
if (failMessages.isEmpty()) {
if (ConfigImpl.traceLoadsEnabled()) {
// 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
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);
for (Throwable t : fails) {
sb.append(t.getMessage());
sb.append(", ");
}
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);
}
} 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 out = new PrintWriter(file)
out.write(config.root.render(renderSettings).lines.
// Strip extra spaces in front and fix additional space in of comments.
map(_.stripPrefix(" ").replaceAll("^(\\s*)# ", "$1# ")).
// Strip extra spaces in front.
map(_.stripPrefix(" ")).
// Indent two spaces instead of four.
map(line => """^(\s*)""".r.replaceAllIn(line, m => m.group(1).replace(" ", " "))).
// Finalize the string.