diff --git a/src/main/java/com/typesafe/config/impl/Parser.java b/src/main/java/com/typesafe/config/impl/Parser.java index d0c14c6e..2248880d 100644 --- a/src/main/java/com/typesafe/config/impl/Parser.java +++ b/src/main/java/com/typesafe/config/impl/Parser.java @@ -326,6 +326,51 @@ final class Parser { } } + private static boolean isIncludeKeyword(Token t) { + return Tokens.isUnquotedText(t) + && Tokens.getUnquotedText(t).equals("include"); + } + + private static boolean isUnquotedWhitespace(Token t) { + if (!Tokens.isUnquotedText(t)) + return false; + + String s = Tokens.getUnquotedText(t); + + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + if (!Character.isWhitespace(c)) + return false; + } + return true; + } + + private void parseInclude(Map<String, AbstractConfigValue> values) { + Token t = nextTokenIgnoringNewline(); + while (isUnquotedWhitespace(t)) { + t = nextTokenIgnoringNewline(); + } + + if (Tokens.isValueWithType(t, ConfigValueType.STRING)) { + String name = (String) Tokens.getValue(t).unwrapped(); + AbstractConfigObject obj = includer.include(name); + + for (String key : obj.keySet()) { + AbstractConfigValue v = obj.get(key); + AbstractConfigValue existing = values.get(key); + if (existing != null) { + values.put(key, v.withFallback(existing)); + } else { + values.put(key, v); + } + } + + } else { + throw parseError("include keyword is not followed by a quoted string, but by: " + + t); + } + } + private AbstractConfigObject parseObject() { // invoked just after the OPEN_CURLY Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>(); @@ -338,6 +383,10 @@ final class Parser { throw parseError("expecting a field name after comma, got a close brace }"); } break; + } else if (flavor != SyntaxFlavor.JSON && isIncludeKeyword(t)) { + parseInclude(values); + + afterComma = false; } else { Path path = parseKey(t); Token afterKey = nextTokenIgnoringNewline(); diff --git a/src/test/resources/test03.conf b/src/test/resources/test03.conf new file mode 100644 index 00000000..54cafe30 --- /dev/null +++ b/src/test/resources/test03.conf @@ -0,0 +1,17 @@ +{ + "test01" : { + "ints" : 12, + include "test01", + "booleans" : 42 + }, + + "test02" : { + include + + "test02.conf" + }, + + "equiv01" : { + include "equiv01/original.json" + } +} diff --git a/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index f66cd3b9..7b742c84 100644 --- a/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -634,4 +634,22 @@ class ConfigTest extends TestUtils { assertEquals(57, conf.getInt(""" "a"."b"."c" """)) assertEquals(103, conf.getInt(""" "a.b.c" """)) } + + @Test + def test03Includes() { + val conf = Config.load("test03") + + // include should have overridden the "ints" value in test03 + assertEquals(42, conf.getInt("test01.ints.fortyTwo")) + // include should have been overridden by 42 + assertEquals(42, conf.getInt("test01.booleans")); + assertEquals(42, conf.getInt("test01.booleans")); + // include should have gotten .properties and .json also + assertEquals("abc", conf.getString("test01.fromProps.abc")) + assertEquals("A", conf.getString("test01.fromJsonA")) + // test02 was included + assertEquals(57, conf.getInt("test02.a.b.c")) + // equiv01/original.json was included (it has a slash in the name) + assertEquals("a", conf.getString("equiv01.strings.a")) + } } diff --git a/src/test/scala/com/typesafe/config/impl/TestUtils.scala b/src/test/scala/com/typesafe/config/impl/TestUtils.scala index e821797d..fcbfba66 100644 --- a/src/test/scala/com/typesafe/config/impl/TestUtils.scala +++ b/src/test/scala/com/typesafe/config/impl/TestUtils.scala @@ -134,6 +134,9 @@ abstract trait TestUtils { """{ "a" : ${a} }""", // simple cycle """[ { "a" : 2, "b" : ${${a}} } ]""", // nested substitution "[ = ]", // = is not a valid token + "{ include \"bar\" : 10 }", // include with a value after it + "{ include foo }", // include with unquoted string + "{ include : { \"a\" : 1 } }", // include used as unquoted key "") // empty document again, just for clean formatting of this list ;-) // We'll automatically try each of these with whitespace modifications @@ -178,6 +181,11 @@ abstract trait TestUtils { "[ trux ]", "[ truex ]", "[ 10x ]", // number token with trailing junk + "{ include \"foo\" }", // valid include + "{ include\n\"foo\" }", // include with just a newline separating from string + "{ include\"foo\" }", // include with no whitespace after it + "[ include ]", // include can be a string value in an array + "{ foo : include }", // include can be a field value also "[ ${foo} ]", "[ ${\"foo\"} ]", "[ ${foo.bar} ]",