diff --git a/com/typesafe/config/Config.java b/com/typesafe/config/Config.java
index 0a72f5b05..42e4df0cb 100644
--- a/com/typesafe/config/Config.java
+++ b/com/typesafe/config/Config.java
@@ -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.
*
+ * @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 the
+ * spec.
+ *
+ * @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 getBytesList(String path);
- List getMillisecondsList(String path);
+ /**
+ * @deprecated As of release 1.1, replaced by {@link #getDurationList(String, TimeUnit)}
+ */
+ @Deprecated List getMillisecondsList(String path);
- List getNanosecondsList(String path);
+ /**
+ * @deprecated As of release 1.1, replaced by {@link #getDurationList(String, TimeUnit)}
+ */
+ @Deprecated List 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 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
diff --git a/com/typesafe/config/ConfigException.java b/com/typesafe/config/ConfigException.java
index 0e9f895f6..df2c7b34d 100644
--- a/com/typesafe/config/ConfigException.java
+++ b/com/typesafe/config/ConfigException.java
@@ -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 + ")";
+ }
}
/**
diff --git a/com/typesafe/config/impl/ConfigImplUtil.java b/com/typesafe/config/impl/ConfigImplUtil.java
index 5ea65537a..fde0341d9 100644
--- a/com/typesafe/config/impl/ConfigImplUtil.java
+++ b/com/typesafe/config/impl/ConfigImplUtil.java
@@ -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);
}
diff --git a/com/typesafe/config/impl/Parseable.java b/com/typesafe/config/impl/Parseable.java
index 16f3cb969..35c750015 100644
--- a/com/typesafe/config/impl/Parseable.java
+++ b/com/typesafe/config/impl/Parseable.java
@@ -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);
diff --git a/com/typesafe/config/impl/Parser.java b/com/typesafe/config/impl/Parser.java
index 13331d35e..94c6189ad 100644
--- a/com/typesafe/config/impl/Parser.java
+++ b/com/typesafe/config/impl/Parser.java
@@ -41,14 +41,26 @@ final class Parser {
TokenWithComments(Token token, List 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. emptyList());
}
+ TokenWithComments removeAll() {
+ if (comments.isEmpty())
+ return this;
+ else
+ return new TokenWithComments(token);
+ }
+
TokenWithComments prepend(List earlier) {
- if (this.comments.isEmpty()) {
+ if (earlier.isEmpty()) {
+ return this;
+ } else if (this.comments.isEmpty()) {
return new TokenWithComments(token, earlier);
} else {
List merged = new ArrayList();
@@ -58,7 +70,18 @@ final class Parser {
}
}
- SimpleConfigOrigin setComments(SimpleConfigOrigin origin) {
+ TokenWithComments add(Token after) {
+ if (this.comments.isEmpty()) {
+ return new TokenWithComments(token, Collections. singletonList(after));
+ } else {
+ List merged = new ArrayList();
+ 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 newComments = new ArrayList();
+ 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 newlines = new ArrayList();
List comments = new ArrayList();
@@ -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 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();
- 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 values = new HashMap();
- 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 values = new ArrayList();
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. 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);
diff --git a/com/typesafe/config/impl/SerializedConfigValue.java b/com/typesafe/config/impl/SerializedConfigValue.java
index 559708575..a18a18997 100644
--- a/com/typesafe/config/impl/SerializedConfigValue.java
+++ b/com/typesafe/config/impl/SerializedConfigValue.java
@@ -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 m = origin.toFieldsDelta(baseOrigin);
+ Map 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 e : m.entrySet()) {
FieldOut field = new FieldOut(e.getKey());
Object v = e.getValue();
diff --git a/com/typesafe/config/impl/SimpleConfig.java b/com/typesafe/config/impl/SimpleConfig.java
index d368e1446..d3be5cf43 100644
--- a/com/typesafe/config/impl/SimpleConfig.java
+++ b/com/typesafe/config/impl/SimpleConfig.java
@@ -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 getMillisecondsList(String path) {
- List nanos = getNanosecondsList(path);
- List l = new ArrayList();
- for (Long n : nanos) {
- l.add(TimeUnit.NANOSECONDS.toMillis(n));
- }
- return l;
- }
-
- @Override
- public List getNanosecondsList(String path) {
+ public List getDurationList(String path, TimeUnit unit) {
List l = new ArrayList();
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 getMillisecondsList(String path) {
+ return getDurationList(path, TimeUnit.MILLISECONDS);
+ }
+
+ @Deprecated
+ @Override
+ public List 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 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 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);
diff --git a/com/typesafe/config/impl/SimpleConfigObject.java b/com/typesafe/config/impl/SimpleConfigObject.java
index fb3ac60df..1977bd1ba 100644
--- a/com/typesafe/config/impl/SimpleConfigObject.java
+++ b/com/typesafe/config/impl/SimpleConfigObject.java
@@ -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");
}
diff --git a/com/typesafe/config/impl/SimpleConfigOrigin.java b/com/typesafe/config/impl/SimpleConfigOrigin.java
index 353e9c051..457597a37 100644
--- a/com/typesafe/config/impl/SimpleConfigOrigin.java
+++ b/com/typesafe/config/impl/SimpleConfigOrigin.java
@@ -93,6 +93,34 @@ final class SimpleConfigOrigin implements ConfigOrigin {
}
}
+ SimpleConfigOrigin prependComments(List comments) {
+ if (ConfigImplUtil.equalsHandlingNull(comments, this.commentsOrNull) || comments == null) {
+ return this;
+ } else if (this.commentsOrNull == null) {
+ return setComments(comments);
+ } else {
+ List merged = new ArrayList(comments.size()
+ + this.commentsOrNull.size());
+ merged.addAll(comments);
+ merged.addAll(this.commentsOrNull);
+ return setComments(merged);
+ }
+ }
+
+ SimpleConfigOrigin appendComments(List comments) {
+ if (ConfigImplUtil.equalsHandlingNull(comments, this.commentsOrNull) || comments == null) {
+ return this;
+ } else if (this.commentsOrNull == null) {
+ return setComments(comments);
+ } else {
+ List merged = new ArrayList(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 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);
diff --git a/com/typesafe/config/impl/SimpleIncluder.java b/com/typesafe/config/impl/SimpleIncluder.java
index e3cd25131..2cde2f92e 100644
--- a/com/typesafe/config/impl/SimpleIncluder.java
+++ b/com/typesafe/config/impl/SimpleIncluder.java
@@ -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 failMessages = new ArrayList();
+ List fails = new ArrayList();
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);
}
}
diff --git a/com/typesafe/config/impl/Tokenizer.java b/com/typesafe/config/impl/Tokenizer.java
index 4332aa7ba..0da230702 100644
--- a/com/typesafe/config/impl/Tokenizer.java
+++ b/com/typesafe/config/impl/Tokenizer.java
@@ -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);
}
}
diff --git a/li/cil/oc/Settings.scala b/li/cil/oc/Settings.scala
index 91b78f682..cbb2c0cbf 100644
--- a/li/cil/oc/Settings.scala
+++ b/li/cil/oc/Settings.scala
@@ -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.