From 58b19f5fa4eda7f50df03d00ddd418b418cb43d8 Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Wed, 9 Nov 2011 11:07:46 -0500 Subject: [PATCH] test and fix up the merged config object loaded from resources --- .../com/typesafe/config/ConfigObject.java | 2 + .../config/impl/AbstractConfigObject.java | 14 +- .../com/typesafe/config/impl/ConfigImpl.java | 55 +++- .../java/com/typesafe/config/impl/Parser.java | 52 +++- src/test/resources/test01.conf | 55 ++++ src/test/resources/test01.json | 4 + src/test/resources/test01.properties | 4 + .../config/impl/ConfigSubstitutionTest.scala | 10 - .../com/typesafe/config/impl/ConfigTest.scala | 236 ++++++++++++++++++ .../com/typesafe/config/impl/TestUtils.scala | 11 + 10 files changed, 415 insertions(+), 28 deletions(-) create mode 100644 src/test/resources/test01.conf create mode 100644 src/test/resources/test01.json create mode 100644 src/test/resources/test01.properties create mode 100644 src/test/scala/com/typesafe/config/impl/ConfigTest.scala diff --git a/src/main/java/com/typesafe/config/ConfigObject.java b/src/main/java/com/typesafe/config/ConfigObject.java index cd9cb4e1..8ad71b99 100644 --- a/src/main/java/com/typesafe/config/ConfigObject.java +++ b/src/main/java/com/typesafe/config/ConfigObject.java @@ -90,6 +90,8 @@ public interface ConfigObject extends ConfigValue { List getDoubleList(String path); + List getStringList(String path); + List getObjectList(String path); List getAnyList(String path); diff --git a/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java b/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java index 57add19c..24b08e7a 100644 --- a/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -146,10 +146,9 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements } /** - * Stack should be from overrides to fallbacks (earlier items win). Test - * suite should check: merging of objects with a non-object in the middle. - * Override of object with non-object, override of non-object with object. - * Merging 0, 1, N objects. + * Stack should be from overrides to fallbacks (earlier items win). Objects + * have their keys combined into a new object, while other kinds of value + * are just first-one-wins. */ static AbstractConfigObject merge(ConfigOrigin origin, List stack, @@ -175,6 +174,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements stackForKey = objects.get(key); } else { stackForKey = new ArrayList(); + objects.put(key, stackForKey); } stackForKey.add(transformed( (AbstractConfigObject) v, @@ -187,6 +187,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements } } } + for (Map.Entry> entry : objects .entrySet()) { List stackForKey = entry.getValue(); @@ -373,6 +374,11 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements return l; } + @Override + public List getStringList(String path) { + return getHomogeneousUnwrappedList(path, ConfigValueType.STRING); + } + @Override public List getObjectList(String path) { List l = new ArrayList(); diff --git a/src/main/java/com/typesafe/config/impl/ConfigImpl.java b/src/main/java/com/typesafe/config/impl/ConfigImpl.java index 4023a4fc..ff61cde5 100644 --- a/src/main/java/com/typesafe/config/impl/ConfigImpl.java +++ b/src/main/java/com/typesafe/config/impl/ConfigImpl.java @@ -1,5 +1,8 @@ package com.typesafe.config.impl; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; @@ -11,6 +14,7 @@ import java.util.Properties; import com.typesafe.config.ConfigConfig; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigOrigin; /** This is public but is only supposed to be used by the "config" package */ public class ConfigImpl { @@ -28,13 +32,29 @@ public class ConfigImpl { if (system != null) stack.add(system); + // now try to load a resource for each extension + addResource(configConfig.rootPath() + ".conf", stack); + addResource(configConfig.rootPath() + ".json", stack); + addResource(configConfig.rootPath() + ".properties", stack); + ConfigTransformer transformer = withExtraTransformer(null); AbstractConfigObject merged = AbstractConfigObject .merge(new SimpleConfigOrigin("config for " + configConfig.rootPath()), stack, transformer); - return merged; + AbstractConfigValue resolved = SubstitutionResolver.resolve(merged, + merged); + + return (AbstractConfigObject) resolved; + } + + private static void addResource(String name, + List stack) { + URL url = ConfigImpl.class.getResource("/" + name); + if (url != null) { + stack.add(loadURL(url)); + } } static ConfigObject getEnvironmentAsConfig() { @@ -51,6 +71,39 @@ public class ConfigImpl { withExtraTransformer(null)); } + static AbstractConfigObject loadURL(URL url) { + if (url.getPath().endsWith(".properties")) { + ConfigOrigin origin = new SimpleConfigOrigin(url.toExternalForm()); + Properties props = new Properties(); + InputStream stream = null; + try { + stream = url.openStream(); + props.load(stream); + } catch (IOException e) { + throw new ConfigException.IO(origin, "failed to open url", e); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + return fromProperties(url.toExternalForm(), props); + } else { + return forceParsedToObject(Parser.parse(url)); + } + } + + static AbstractConfigObject forceParsedToObject(AbstractConfigValue value) { + if (value instanceof AbstractConfigObject) { + return (AbstractConfigObject) value; + } else { + throw new ConfigException.WrongType(value.origin(), "", + "object at file root", value.valueType().name()); + } + } + private static ConfigTransformer withExtraTransformer( ConfigTransformer extraTransformer) { // idea is to avoid creating a new, unique transformer if there's no diff --git a/src/main/java/com/typesafe/config/impl/Parser.java b/src/main/java/com/typesafe/config/impl/Parser.java index f3afdf20..27195edb 100644 --- a/src/main/java/com/typesafe/config/impl/Parser.java +++ b/src/main/java/com/typesafe/config/impl/Parser.java @@ -2,13 +2,14 @@ package com.typesafe.config.impl; import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -48,28 +49,53 @@ final class Parser { return parse(flavor, origin, new StringReader(input)); } - static AbstractConfigValue parse(File f) { - ConfigOrigin origin = new SimpleConfigOrigin(f.getPath()); - SyntaxFlavor flavor = null; - if (f.getName().endsWith(".json")) - flavor = SyntaxFlavor.JSON; - else if (f.getName().endsWith(".conf")) - flavor = SyntaxFlavor.CONF; + private static SyntaxFlavor flavorFromExtension(String name, + ConfigOrigin origin) { + if (name.endsWith(".json")) + return SyntaxFlavor.JSON; + else if (name.endsWith(".conf")) + return SyntaxFlavor.CONF; else throw new ConfigException.IO(origin, "Unknown filename extension"); - return parse(flavor, f); + } + + static AbstractConfigValue parse(File f) { + return parse(null, f); } static AbstractConfigValue parse(SyntaxFlavor flavor, File f) { ConfigOrigin origin = new SimpleConfigOrigin(f.getPath()); + try { + return parse(flavor, origin, f.toURI().toURL()); + } catch (MalformedURLException e) { + throw new ConfigException.IO(origin, + "failed to create url from file path", e); + } + } + static AbstractConfigValue parse(URL url) { + return parse(null, url); + } + + static AbstractConfigValue parse(SyntaxFlavor flavor, URL url) { + ConfigOrigin origin = new SimpleConfigOrigin(url.toExternalForm()); + return parse(flavor, origin, url); + } + + static AbstractConfigValue parse(SyntaxFlavor flavor, ConfigOrigin origin, + URL url) { AbstractConfigValue result = null; try { - InputStream stream = new BufferedInputStream(new FileInputStream(f)); - result = parse(flavor, origin, stream); - stream.close(); + InputStream stream = new BufferedInputStream(url.openStream()); + try { + result = parse( + flavor != null ? flavor : flavorFromExtension( + url.getPath(), origin), origin, stream); + } finally { + stream.close(); + } } catch (IOException e) { - throw new ConfigException.IO(origin, "failed to read file", e); + throw new ConfigException.IO(origin, "failed to read url", e); } return result; } diff --git a/src/test/resources/test01.conf b/src/test/resources/test01.conf new file mode 100644 index 00000000..68d0c9cc --- /dev/null +++ b/src/test/resources/test01.conf @@ -0,0 +1,55 @@ +{ + "ints" : { + "fortyTwo" : 42, + "fortyTwoAgain" : ${ints.fortyTwo} + }, + + "floats" : { + "fortyTwoPointOne" : 42.1, + "fortyTwoPointOneAgain" : ${floats.fortyTwoPointOne} + }, + + "strings" : { + "abcd" : "abcd", + "abcdAgain" : ${strings.a}${strings.b}${strings.c}${strings.d}, + "a" : "a", + "b" : "b", + "c" : "c", + "d" : "d", + "concatenated" : null bar 42 baz true 3.14 hi, + "number" : "57" + }, + + "arrays" : { + "empty" : [], + "ofInt" : [1, 2, 3], + "ofString" : [ ${strings.a}, ${strings.b}, ${strings.c} ], + "ofDouble" : [3.14, 4.14, 5.14], + "ofNull" : [null, null, null], + "ofBoolean" : [true, false], + "ofArray" : [${arrays.ofString}, ${arrays.ofString}, ${arrays.ofString}], + "ofObject" : [${ints}, ${booleans}, ${strings}] + }, + + "booleans" : { + "true" : true, + "trueAgain" : ${booleans.true}, + "false" : false, + "falseAgain" : ${booleans.false} + }, + + "nulls" : { + "null" : null, + "nullAgain" : null + }, + + "durations" : { + "second" : 1s, + "secondsList" : [1s,2seconds,3 s] + }, + + "memsizes" : { + "meg" : 1M, + "megsList" : [1M, 1024K] + } +} diff --git a/src/test/resources/test01.json b/src/test/resources/test01.json new file mode 100644 index 00000000..13809157 --- /dev/null +++ b/src/test/resources/test01.json @@ -0,0 +1,4 @@ +{ + "fromJson1" : 1, + "fromJsonA" : "A" +} \ No newline at end of file diff --git a/src/test/resources/test01.properties b/src/test/resources/test01.properties new file mode 100644 index 00000000..94eec447 --- /dev/null +++ b/src/test/resources/test01.properties @@ -0,0 +1,4 @@ +# .properties file +fromProps.abc=abc +fromProps.one=1 +fromProps.bool=true diff --git a/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala b/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala index e119565f..f6797933 100644 --- a/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala +++ b/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala @@ -7,10 +7,6 @@ import com.typesafe.config.ConfigException class ConfigSubstitutionTest extends TestUtils { - private def parseObject(s: String) = { - Parser.parse(SyntaxFlavor.CONF, new SimpleConfigOrigin("test string"), s).asInstanceOf[AbstractConfigObject] - } - private def subst(ref: String, style: SubstitutionStyle = SubstitutionStyle.PATH) = { val pieces = java.util.Collections.singletonList[Object](new Substitution(ref, style)) new ConfigSubstitution(fakeOrigin(), pieces) @@ -22,12 +18,6 @@ class ConfigSubstitutionTest extends TestUtils { new ConfigSubstitution(fakeOrigin(), pieces.asJava) } - private def intValue(i: Int) = new ConfigInt(fakeOrigin(), i) - private def boolValue(b: Boolean) = new ConfigBoolean(fakeOrigin(), b) - private def nullValue() = new ConfigNull(fakeOrigin()) - private def stringValue(s: String) = new ConfigString(fakeOrigin(), s) - private def doubleValue(d: Double) = new ConfigDouble(fakeOrigin(), d) - private def resolveWithoutFallbacks(v: AbstractConfigObject) = { SubstitutionResolver.resolveWithoutFallbacks(v, v).asInstanceOf[AbstractConfigObject] } diff --git a/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala new file mode 100644 index 00000000..f746120f --- /dev/null +++ b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -0,0 +1,236 @@ +package com.typesafe.config.impl + +import org.junit.Assert._ +import org.junit._ +import com.typesafe.config.ConfigValue +import com.typesafe.config.Config +import com.typesafe.config.ConfigObject +import com.typesafe.config.ConfigException +import java.util.concurrent.TimeUnit +import scala.collection.JavaConverters._ + +class ConfigTest extends TestUtils { + + @Test + def mergeTrivial() { + val obj1 = parseObject("""{ "a" : 1 }""") + val obj2 = parseObject("""{ "b" : 2 }""") + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2).asJava, null) + + assertEquals(1, merged.getInt("a")) + assertEquals(2, merged.getInt("b")) + assertEquals(2, merged.keySet().size) + } + + @Test + def mergeEmpty() { + val merged = AbstractConfigObject.merge(fakeOrigin(), List[AbstractConfigObject]().asJava, null) + + assertEquals(0, merged.keySet().size) + } + + @Test + def mergeOne() { + val obj1 = parseObject("""{ "a" : 1 }""") + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1).asJava, null) + + assertEquals(1, merged.getInt("a")) + assertEquals(1, merged.keySet().size) + } + + @Test + def mergeOverride() { + val obj1 = parseObject("""{ "a" : 1 }""") + val obj2 = parseObject("""{ "a" : 2 }""") + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2).asJava, null) + + assertEquals(1, merged.getInt("a")) + assertEquals(1, merged.keySet().size) + + val merged2 = AbstractConfigObject.merge(fakeOrigin(), List(obj2, obj1).asJava, null) + + assertEquals(2, merged2.getInt("a")) + assertEquals(1, merged2.keySet().size) + } + + @Test + def mergeN() { + val obj1 = parseObject("""{ "a" : 1 }""") + val obj2 = parseObject("""{ "b" : 2 }""") + val obj3 = parseObject("""{ "c" : 3 }""") + val obj4 = parseObject("""{ "d" : 4 }""") + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2, obj3, obj4).asJava, null) + + assertEquals(1, merged.getInt("a")) + assertEquals(2, merged.getInt("b")) + assertEquals(3, merged.getInt("c")) + assertEquals(4, merged.getInt("d")) + assertEquals(4, merged.keySet().size) + } + + @Test + def mergeOverrideN() { + val obj1 = parseObject("""{ "a" : 1 }""") + val obj2 = parseObject("""{ "a" : 2 }""") + val obj3 = parseObject("""{ "a" : 3 }""") + val obj4 = parseObject("""{ "a" : 4 }""") + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2, obj3, obj4).asJava, null) + + assertEquals(1, merged.getInt("a")) + assertEquals(1, merged.keySet().size) + + val merged2 = AbstractConfigObject.merge(fakeOrigin(), List(obj4, obj3, obj2, obj1).asJava, null) + + assertEquals(4, merged2.getInt("a")) + assertEquals(1, merged2.keySet().size) + } + + @Test + def mergeNested() { + val obj1 = parseObject("""{ "root" : { "a" : 1, "z" : 101 } }""") + val obj2 = parseObject("""{ "root" : { "b" : 2, "z" : 102 } }""") + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2).asJava, null) + + assertEquals(1, merged.getInt("root.a")) + assertEquals(2, merged.getInt("root.b")) + assertEquals(101, merged.getInt("root.z")) + assertEquals(1, merged.keySet().size) + assertEquals(3, merged.getObject("root").keySet().size) + } + + @Test + def mergeOverrideObjectAndPrimitive() { + val obj1 = parseObject("""{ "a" : 1 }""") + val obj2 = parseObject("""{ "a" : { "b" : 42 } }""") + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2).asJava, null) + + assertEquals(1, merged.getInt("a")) + assertEquals(1, merged.keySet().size) + + val merged2 = AbstractConfigObject.merge(fakeOrigin(), List(obj2, obj1).asJava, null) + + assertEquals(42, merged2.getObject("a").getInt("b")) + assertEquals(42, merged2.getInt("a.b")) + assertEquals(1, merged2.keySet().size) + assertEquals(1, merged2.getObject("a").keySet().size) + } + + @Test + def mergeObjectThenPrimitiveThenObject() { + val obj1 = parseObject("""{ "a" : { "b" : 42 } }""") + val obj2 = parseObject("""{ "a" : 2 }""") + val obj3 = parseObject("""{ "a" : { "b" : 43 } }""") + + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2, obj3).asJava, null) + + assertEquals(42, merged.getInt("a.b")) + assertEquals(1, merged.keySet().size) + assertEquals(1, merged.getObject("a").keySet().size()) + } + + @Test + def mergePrimitiveThenObjectThenPrimitive() { + val obj1 = parseObject("""{ "a" : 1 }""") + val obj2 = parseObject("""{ "a" : { "b" : 42 } }""") + val obj3 = parseObject("""{ "a" : 3 }""") + + val merged = AbstractConfigObject.merge(fakeOrigin(), List(obj1, obj2, obj3).asJava, null) + + assertEquals(1, merged.getInt("a")) + assertEquals(1, merged.keySet().size) + } + + @Test + def test01() { + val conf = Config.load("test01") + + // get all the primitive types + assertEquals(42, conf.getInt("ints.fortyTwo")) + assertEquals(42, conf.getInt("ints.fortyTwoAgain")) + assertEquals(42L, conf.getLong("ints.fortyTwoAgain")) + assertEquals(42.1, conf.getDouble("floats.fortyTwoPointOne"), 1e-6) + assertEquals(42.1, conf.getDouble("floats.fortyTwoPointOneAgain"), 1e-6) + assertEquals("abcd", conf.getString("strings.abcd")) + assertEquals("abcd", conf.getString("strings.abcdAgain")) + assertEquals("null bar 42 baz true 3.14 hi", conf.getString("strings.concatenated")) + assertEquals(true, conf.getBoolean("booleans.trueAgain")) + assertEquals(false, conf.getBoolean("booleans.falseAgain")) + // FIXME need to add a way to get a null + //assertEquals(null, conf.getAny("nulls.null")) + + // get empty array as any type of array + assertEquals(Seq(), conf.getAnyList("arrays.empty").asScala) + assertEquals(Seq(), conf.getIntList("arrays.empty").asScala) + assertEquals(Seq(), conf.getLongList("arrays.empty").asScala) + assertEquals(Seq(), conf.getStringList("arrays.empty").asScala) + assertEquals(Seq(), conf.getLongList("arrays.empty").asScala) + assertEquals(Seq(), conf.getDoubleList("arrays.empty").asScala) + assertEquals(Seq(), conf.getObjectList("arrays.empty").asScala) + assertEquals(Seq(), conf.getBooleanList("arrays.empty").asScala) + assertEquals(Seq(), conf.getNumberList("arrays.empty").asScala) + assertEquals(Seq(), conf.getList("arrays.empty").asScala) + + // get typed arrays + assertEquals(Seq(1, 2, 3), conf.getIntList("arrays.ofInt").asScala) + assertEquals(Seq(1L, 2L, 3L), conf.getLongList("arrays.ofInt").asScala) + assertEquals(Seq("a", "b", "c"), conf.getStringList("arrays.ofString").asScala) + assertEquals(Seq(3.14, 4.14, 5.14), conf.getDoubleList("arrays.ofDouble").asScala) + assertEquals(Seq(null, null, null), conf.getAnyList("arrays.ofNull").asScala) + assertEquals(Seq(true, false), conf.getBooleanList("arrays.ofBoolean").asScala) + val listOfLists = conf.getAnyList("arrays.ofArray").asScala map { _.asInstanceOf[java.util.List[_]].asScala } + assertEquals(Seq(Seq("a", "b", "c"), Seq("a", "b", "c"), Seq("a", "b", "c")), listOfLists) + assertEquals(3, conf.getObjectList("arrays.ofObject").asScala.length) + + // plain getList should work + assertEquals(Seq(intValue(1), intValue(2), intValue(3)), conf.getList("arrays.ofInt").asScala) + assertEquals(Seq(stringValue("a"), stringValue("b"), stringValue("c")), conf.getList("arrays.ofString").asScala) + + // should throw Missing if key doesn't exist + intercept[ConfigException.Missing] { + conf.getInt("doesnotexist") + } + + // should throw Null if key is null + intercept[ConfigException.Null] { + conf.getInt("nulls.null") + } + + // should throw WrongType if key is wrong type and not convertible + intercept[ConfigException.WrongType] { + conf.getInt("booleans.trueAgain") + } + + // should convert numbers to string + assertEquals("42", conf.getString("ints.fortyTwo")) + assertEquals("42.1", conf.getString("floats.fortyTwoPointOne")) + + // should convert string to number + assertEquals(57, conf.getInt("strings.number")) + + // should get durations + def asNanos(secs: Int) = TimeUnit.SECONDS.toNanos(secs) + assertEquals(1000L, conf.getMilliseconds("durations.second")) + assertEquals(asNanos(1), conf.getNanoseconds("durations.second")) + assertEquals(Seq(1000L, 2000L, 3000L), + conf.getMillisecondsList("durations.secondsList").asScala) + assertEquals(Seq(asNanos(1), asNanos(2), asNanos(3)), + conf.getNanosecondsList("durations.secondsList").asScala) + + // should get size in bytes + assertEquals(1024 * 1024L, conf.getMemorySize("memsizes.meg")) + assertEquals(Seq(1024 * 1024L, 1024 * 1024L), + conf.getMemorySizeList("memsizes.megsList").asScala) + + // should have loaded stuff from .json + assertEquals(1, conf.getInt("fromJson1")) + assertEquals("A", conf.getString("fromJsonA")) + + // should have loaded stuff from .properties + assertEquals("abc", conf.getString("fromProps.abc")) + assertEquals(1, conf.getInt("fromProps.one")) + assertEquals(true, conf.getBoolean("fromProps.bool")) + + // toString() on conf objects doesn't throw (toString is just a debug string so not testing its result) + conf.toString() + } +} diff --git a/src/test/scala/com/typesafe/config/impl/TestUtils.scala b/src/test/scala/com/typesafe/config/impl/TestUtils.scala index 09aab253..59e4dce8 100644 --- a/src/test/scala/com/typesafe/config/impl/TestUtils.scala +++ b/src/test/scala/com/typesafe/config/impl/TestUtils.scala @@ -157,4 +157,15 @@ abstract trait TestUtils { } } } + + protected def intValue(i: Int) = new ConfigInt(fakeOrigin(), i) + protected def boolValue(b: Boolean) = new ConfigBoolean(fakeOrigin(), b) + protected def nullValue() = new ConfigNull(fakeOrigin()) + protected def stringValue(s: String) = new ConfigString(fakeOrigin(), s) + protected def doubleValue(d: Double) = new ConfigDouble(fakeOrigin(), d) + + protected def parseObject(s: String) = { + Parser.parse(SyntaxFlavor.CONF, new SimpleConfigOrigin("test string"), s).asInstanceOf[AbstractConfigObject] + } + }