diff --git a/HOCON.md b/HOCON.md
index 6a00fefc..6869e13d 100644
--- a/HOCON.md
+++ b/HOCON.md
@@ -863,7 +863,7 @@ usual the comma may be omitted if there's a newline).
If an unquoted `include` at the start of a key is followed by
anything other than a single quoted string or the
-`url("")`/`file("")/`classpath("")` syntax, it is invalid and an
+`url("")`/`file("")`/`classpath("")` syntax, it is invalid and an
error should be generated.
There can be any amount of whitespace, including newlines, between
@@ -1209,9 +1209,9 @@ parsed as a number plus an optional unit string.
The supported unit strings for duration are case sensitive and
must be lowercase. Exactly these strings are supported:
- - `ns`, `nanosecond`, `nanoseconds`
- - `us`, `microsecond`, `microseconds`
- - `ms`, `millisecond`, `milliseconds`
+ - `ns`, `nano`, `nanos`, `nanosecond`, `nanoseconds`
+ - `us`, `micro`, `micros`, `microsecond`, `microseconds`
+ - `ms`, `milli`, `millis`, `millisecond`, `milliseconds`
- `s`, `second`, `seconds`
- `m`, `minute`, `minutes`
- `h`, `hour`, `hours`
@@ -1274,6 +1274,13 @@ spec copies that. You can certainly find examples of mapping these
to powers of ten, though. If you don't like ambiguity, don't use
the single-letter abbreviations.
+Note: any value in zetta/zebi or yotta/yobi will overflow a 64-bit
+integer, and of course large-enough values in any of the units may
+overflow. Most real-world APIs and apps will not support byte
+counts that overflow a 64-bit integer. The huge units are provided
+just to be complete but probably aren't useful in practice. At
+least not in 2014.
+
### Config object merging and file merging
It may be useful to offer a method to merge two objects. If such a
diff --git a/README.md b/README.md
index 3725cc5d..0015d84b 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,10 @@ Maven Central.
1.2.1
+sbt dependency:
+
+ libraryDependencies += "com.typesafe" % "config" % "1.2.1"
+
Link for direct download if you don't use a dependency manager:
- http://central.maven.org/maven2/com/typesafe/config/
@@ -743,3 +747,7 @@ format.
#### Ruby port
* https://github.com/cprice404/ruby-hocon
+
+#### Python port
+
+ * pyhocon https://github.com/chimpler/pyhocon
diff --git a/config/build.sbt b/config/build.sbt
index d98130a8..511a4fe1 100644
--- a/config/build.sbt
+++ b/config/build.sbt
@@ -16,7 +16,7 @@ crossPaths := false
libraryDependencies += "net.liftweb" %% "lift-json" % "2.5" % "test"
-libraryDependencies += "com.novocode" % "junit-interface" % "0.10-M4" % "test"
+libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test"
externalResolvers += "Scala Tools Snapshots" at "http://scala-tools.org/repo-snapshots/"
diff --git a/config/src/main/java/com/typesafe/config/ConfigFactory.java b/config/src/main/java/com/typesafe/config/ConfigFactory.java
index e59444bf..8ae6bf87 100644
--- a/config/src/main/java/com/typesafe/config/ConfigFactory.java
+++ b/config/src/main/java/com/typesafe/config/ConfigFactory.java
@@ -591,10 +591,35 @@ public final class ConfigFactory {
return parseURL(url, ConfigParseOptions.defaults());
}
+ /**
+ * Parses a file into a Config instance. Does not call
+ * {@link Config#resolve} or merge the file with any other
+ * configuration; this method parses a single file and does
+ * nothing else. It does process "include" statements in the
+ * parsed file, and may end up doing other IO due to those
+ * statements.
+ *
+ * @param file
+ * the file to parse
+ * @param options
+ * parse options to control how the file is interpreted
+ * @return the parsed configuration
+ * @throws ConfigException on IO or parse errors
+ */
public static Config parseFile(File file, ConfigParseOptions options) {
return Parseable.newFile(file, options).parse().toConfig();
}
+ /**
+ * Parses a file into a Config instance as with
+ * {@link #parseFile(File,ConfigParseOptions)} but always uses the
+ * default parse options.
+ *
+ * @param file
+ * the file to parse
+ * @return the parsed configuration
+ * @throws ConfigException on IO or parse errors
+ */
public static Config parseFile(File file) {
return parseFile(file, ConfigParseOptions.defaults());
}
@@ -637,6 +662,14 @@ public final class ConfigFactory {
return ConfigImpl.parseFileAnySyntax(fileBasename, options).toConfig();
}
+ /**
+ * Like {@link #parseFileAnySyntax(File,ConfigParseOptions)} but always uses
+ * default parse options.
+ *
+ * @param fileBasename
+ * a filename with or without extension
+ * @return the parsed configuration
+ */
public static Config parseFileAnySyntax(File fileBasename) {
return parseFileAnySyntax(fileBasename, ConfigParseOptions.defaults());
}
diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java
index 277c2d04..17e1e6a4 100644
--- a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java
+++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java
@@ -5,6 +5,8 @@ package com.typesafe.config.impl;
import java.io.ObjectStreamException;
import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
@@ -512,12 +514,12 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
unitString = unitString + "s";
// note that this is deliberately case-sensitive
- if (unitString.equals("") || unitString.equals("ms")
+ if (unitString.equals("") || unitString.equals("ms") || unitString.equals("millis")
|| unitString.equals("milliseconds")) {
units = TimeUnit.MILLISECONDS;
- } else if (unitString.equals("us") || unitString.equals("microseconds")) {
+ } else if (unitString.equals("us") || unitString.equals("micros") || unitString.equals("microseconds")) {
units = TimeUnit.MICROSECONDS;
- } else if (unitString.equals("ns") || unitString.equals("nanoseconds")) {
+ } else if (unitString.equals("ns") || unitString.equals("nanos") || unitString.equals("nanoseconds")) {
units = TimeUnit.NANOSECONDS;
} else if (unitString.equals("d") || unitString.equals("days")) {
units = TimeUnit.DAYS;
@@ -575,19 +577,13 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
final String prefix;
final int powerOf;
final int power;
- final long bytes;
+ final BigInteger bytes;
MemoryUnit(String prefix, int powerOf, int power) {
this.prefix = prefix;
this.powerOf = powerOf;
this.power = power;
- int i = power;
- long bytes = 1;
- while (i > 0) {
- bytes *= powerOf;
- --i;
- }
- this.bytes = bytes;
+ this.bytes = BigInteger.valueOf(powerOf).pow(power);
}
private static Map makeUnitsMap() {
@@ -667,13 +663,20 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
}
try {
+ BigInteger result;
// if the string is purely digits, parse as an integer to avoid
// possible precision loss; otherwise as a double.
if (numberString.matches("[0-9]+")) {
- return Long.parseLong(numberString) * units.bytes;
+ result = units.bytes.multiply(new BigInteger(numberString));
} else {
- return (long) (Double.parseDouble(numberString) * units.bytes);
+ BigDecimal resultDecimal = (new BigDecimal(units.bytes)).multiply(new BigDecimal(numberString));
+ result = resultDecimal.toBigInteger();
}
+ if (result.bitLength() < 64)
+ return result.longValue();
+ else
+ throw new ConfigException.BadValue(originForException, pathForException,
+ "size-in-bytes value is out of range for a 64-bit long: '" + input + "'");
} catch (NumberFormatException e) {
throw new ConfigException.BadValue(originForException, pathForException,
"Could not parse size-in-bytes number '" + numberString + "'");
diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java
index 77ad5c6a..49387177 100644
--- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java
+++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java
@@ -115,7 +115,11 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
}
if (changed != null) {
- return new SimpleConfigList(origin(), changed, newResolveStatus);
+ if (newResolveStatus != null) {
+ return new SimpleConfigList(origin(), changed, newResolveStatus);
+ } else {
+ return new SimpleConfigList(origin(), changed);
+ }
} else {
return this;
}
@@ -151,7 +155,7 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
} else {
try {
ResolveModifier modifier = new ResolveModifier(context, source.pushParent(this));
- SimpleConfigList value = modifyMayThrow(modifier, ResolveStatus.RESOLVED);
+ SimpleConfigList value = modifyMayThrow(modifier, context.options().getAllowUnresolved() ? null : ResolveStatus.RESOLVED);
return ResolveResult.make(modifier.context, value);
} catch (NotPossibleToResolve e) {
throw e;
diff --git a/config/src/main/java/com/typesafe/config/impl/Tokenizer.java b/config/src/main/java/com/typesafe/config/impl/Tokenizer.java
index 0da23070..03460ed7 100644
--- a/config/src/main/java/com/typesafe/config/impl/Tokenizer.java
+++ b/config/src/main/java/com/typesafe/config/impl/Tokenizer.java
@@ -457,23 +457,22 @@ final class Tokenizer {
private Token pullQuotedString() throws ProblemException {
// the open quote has already been consumed
StringBuilder sb = new StringBuilder();
- int c = '\0'; // value doesn't get used
- do {
- c = nextCharRaw();
+ while (true) {
+ int c = nextCharRaw();
if (c == -1)
throw problem("End of input but string quote was still open");
if (c == '\\') {
pullEscapeSequence(sb);
} else if (c == '"') {
- // end the loop, done!
+ break;
} else if (Character.isISOControl(c)) {
throw problem(asString(c), "JSON does not allow unescaped " + asString(c)
+ " in quoted strings, use a backslash escape");
} else {
sb.appendCodePoint(c);
}
- } while (c != '"');
+ }
// maybe switch to triple-quoted string, sort of hacky...
if (sb.length() == 0) {
diff --git a/config/src/test/resources/test01.conf b/config/src/test/resources/test01.conf
index eae7f69e..586d23f3 100644
--- a/config/src/test/resources/test01.conf
+++ b/config/src/test/resources/test01.conf
@@ -54,7 +54,9 @@
"second" : 1s,
"secondsList" : [1s,2seconds,3 s, 4000],
"secondAsNumber" : 1000,
- "halfSecond" : 0.5s
+ "halfSecond" : 0.5s,
+ "millis" : 1 milli,
+ "micros" : 2000 micros
},
"memsizes" : {
diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala
index a68f3897..634c0d69 100644
--- a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala
+++ b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala
@@ -769,6 +769,8 @@ class ConfigTest extends TestUtils {
assertEquals(Seq(1, 2, 3, 4) map s2unit,
conf.getDurationList("durations.secondsList", unit).asScala)
assertEquals(ms2unit(500L), conf.getDuration("durations.halfSecond", unit))
+ assertEquals(ms2unit(1L), conf.getDuration("durations.millis", unit))
+ assertEquals(ms2unit(2L), conf.getDuration("durations.micros", unit))
}
assertDurationAsTimeUnit(NANOSECONDS)
@@ -1118,6 +1120,15 @@ class ConfigTest extends TestUtils {
assertTrue("after resolution, config is now resolved", resolved2.isResolved)
}
+ @Test
+ def allowUnresolvedDoesAllowUnresolvedArrayElements() {
+ val values = ConfigFactory.parseString("unknown = [someVal], known = 42")
+ val unresolved = ConfigFactory.parseString("concat = [${unknown}[]], sibling = [${unknown}, ${known}]")
+ unresolved.resolve(ConfigResolveOptions.defaults().setAllowUnresolved(true))
+ unresolved.withFallback(values).resolve()
+ unresolved.resolveWith(values)
+ }
+
@Test
def allowUnresolvedDoesAllowUnresolved() {
val values = ConfigFactory.parseString("{ foo = 1, bar = 2, m = 3, n = 4}")
diff --git a/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala
index 4a6a324b..12a1ce52 100644
--- a/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala
+++ b/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala
@@ -50,16 +50,19 @@ class UnitParserTest extends TestUtils {
val conf = parseConfig("foo = 1d")
assertEquals("could get 1d from conf as days",
- 1L, conf.getDuration("foo", TimeUnit.DAYS))
+ 1L, conf.getDuration("foo", TimeUnit.DAYS))
assertEquals("could get 1d from conf as nanos",
- dayInNanos, conf.getNanoseconds("foo"))
+ dayInNanos, conf.getNanoseconds("foo"))
assertEquals("could get 1d from conf as millis",
- TimeUnit.DAYS.toMillis(1), conf.getMilliseconds("foo"))
+ TimeUnit.DAYS.toMillis(1), conf.getMilliseconds("foo"))
}
@Test
def parseMemorySizeInBytes(): Unit = {
- def parseMem(s: String) = SimpleConfig.parseBytes(s, fakeOrigin(), "test")
+ def parseMem(s: String): Long = SimpleConfig.parseBytes(s, fakeOrigin(), "test")
+
+ assertEquals(Long.MaxValue, parseMem(s"${Long.MaxValue} bytes"))
+ assertEquals(Long.MinValue, parseMem(s"${Long.MinValue} bytes"))
val oneMebis = List("1048576", "1048576b", "1048576bytes", "1048576byte",
"1048576 b", "1048576 bytes",
@@ -88,7 +91,7 @@ class UnitParserTest extends TestUtils {
}
var result = 1024L * 1024 * 1024
- for (unit <- Seq("tebi", "pebi", "exbi", "zebi", "yobi")) {
+ for (unit <- Seq("tebi", "pebi", "exbi")) {
val first = unit.substring(0, 1).toUpperCase()
result = result * 1024;
assertEquals(result, parseMem("1" + first))
@@ -99,7 +102,7 @@ class UnitParserTest extends TestUtils {
}
result = 1000L * 1000 * 1000
- for (unit <- Seq("tera", "peta", "exa", "zetta", "yotta")) {
+ for (unit <- Seq("tera", "peta", "exa")) {
val first = unit.substring(0, 1).toUpperCase()
result = result * 1000;
assertEquals(result, parseMem("1" + first + "B"))
@@ -119,4 +122,45 @@ class UnitParserTest extends TestUtils {
}
assertTrue(e2.getMessage().contains("size-in-bytes number"))
}
+
+ // later on we'll want to check this with BigInteger version of getBytes
+ @Test
+ def parseHugeMemorySizes(): Unit = {
+ def parseMem(s: String): Long = SimpleConfig.parseBytes(s, fakeOrigin(), "test")
+ def assertOutOfRange(s: String) = {
+ val fail = intercept[ConfigException.BadValue] {
+ parseMem(s)
+ }
+ assertTrue("number was too big", fail.getMessage.contains("out of range"))
+ }
+
+ import java.math.BigInteger
+ assertOutOfRange(s"${BigInteger.valueOf(Long.MaxValue).add(BigInteger.valueOf(1)).toString} bytes")
+ assertOutOfRange(s"${BigInteger.valueOf(Long.MinValue).subtract(BigInteger.valueOf(1)).toString} bytes")
+
+ var result = 1024L * 1024 * 1024
+ for (unit <- Seq("zebi", "yobi")) {
+ val first = unit.substring(0, 1).toUpperCase()
+ assertOutOfRange("1" + first)
+ assertOutOfRange("1" + first + "i")
+ assertOutOfRange("1" + first + "iB")
+ assertOutOfRange("1" + unit + "byte")
+ assertOutOfRange("1" + unit + "bytes")
+ assertOutOfRange("1.1" + first)
+ assertOutOfRange("-1" + first)
+ }
+
+ result = 1000L * 1000 * 1000
+ for (unit <- Seq("zetta", "yotta")) {
+ val first = unit.substring(0, 1).toUpperCase()
+ assertOutOfRange("1" + first + "B")
+ assertOutOfRange("1" + unit + "byte")
+ assertOutOfRange("1" + unit + "bytes")
+ assertOutOfRange("1.1" + first + "B")
+ assertOutOfRange("-1" + first + "B")
+ }
+
+ assertOutOfRange("1000 exabytes")
+ assertOutOfRange("10000000 petabytes")
+ }
}
diff --git a/project/build.properties b/project/build.properties
index 37b489cb..748703f7 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=0.13.1
+sbt.version=0.13.7