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 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.