diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java index 1523e759..56885fe7 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java @@ -33,7 +33,7 @@ final class ConfigNodeComplexValue extends AbstractConfigNodeValue { continue; } node = (ConfigNodeKeyValue)children.get(i); - key = Path.newPath(node.key().render()); + key = node.key().value(); if (key.equals(desiredPath)) { if (!replaced) { childrenCopy.set(i, node.replaceValue(value)); @@ -67,7 +67,7 @@ final class ConfigNodeComplexValue extends AbstractConfigNodeValue { ArrayList<AbstractConfigNode> childrenCopy = new ArrayList<AbstractConfigNode>(children); ArrayList<AbstractConfigNode> newNodes = new ArrayList(); newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null))); - newNodes.add(new ConfigNodeKey(Tokens.newUnquotedText(null, desiredPath.render()))); + newNodes.add(new ConfigNodeKey(desiredPath)); newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " "))); newNodes.add(new ConfigNodeSingleToken(Tokens.COLON)); newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " "))); diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeKey.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeKey.java index 0dd76eba..7e157847 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeKey.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeKey.java @@ -1,15 +1,18 @@ package com.typesafe.config.impl; import java.util.Collection; -import java.util.Collections; final class ConfigNodeKey extends AbstractConfigNode { - Token token; - ConfigNodeKey(Token t) { - token = t; + final private Path key; + ConfigNodeKey(Path key) { + this.key = key; } protected Collection<Token> tokens() { - return Collections.singletonList(token); + return key.tokens(); + } + + protected Path value() { + return key; } } diff --git a/config/src/main/java/com/typesafe/config/impl/Parser.java b/config/src/main/java/com/typesafe/config/impl/Parser.java index 6287c063..e5b9362f 100644 --- a/config/src/main/java/com/typesafe/config/impl/Parser.java +++ b/config/src/main/java/com/typesafe/config/impl/Parser.java @@ -1063,6 +1063,7 @@ final class Parser { private static Path parsePathExpression(Iterator<Token> expression, ConfigOrigin origin, String originalText) { // each builder in "buf" is an element in the path. + ArrayList<Token> pathTokens = new ArrayList<Token>(); List<Element> buf = new ArrayList<Element>(); buf.add(new Element("", false)); @@ -1073,6 +1074,7 @@ final class Parser { while (expression.hasNext()) { Token t = expression.next(); + pathTokens.add(t); // Ignore all IgnoredWhitespace tokens if (Tokens.isIgnoredWhitespace(t)) @@ -1119,7 +1121,7 @@ final class Parser { } } - PathBuilder pb = new PathBuilder(); + PathBuilder pb = new PathBuilder(pathTokens); for (Element e : buf) { if (e.sb.length() == 0 && !e.canBeEmpty) { throw new ConfigException.BadPath( @@ -1192,8 +1194,10 @@ final class Parser { private static Path fastPathBuild(Path tail, String s, int end) { // lastIndexOf takes last index it should look at, end - 1 not end int splitAt = s.lastIndexOf('.', end - 1); + ArrayList<Token> tokens = new ArrayList<Token>(); + tokens.add(Tokens.newUnquotedText(null, s)); // this works even if splitAt is -1; then we start the substring at 0 - Path withOneMoreElement = new Path(s.substring(splitAt + 1, end), tail); + Path withOneMoreElement = new Path(s.substring(splitAt + 1, end), tail, tokens); if (splitAt < 0) { return withOneMoreElement; } else { diff --git a/config/src/main/java/com/typesafe/config/impl/Path.java b/config/src/main/java/com/typesafe/config/impl/Path.java index 51e5b788..5cb041a1 100644 --- a/config/src/main/java/com/typesafe/config/impl/Path.java +++ b/config/src/main/java/com/typesafe/config/impl/Path.java @@ -3,6 +3,8 @@ */ package com.typesafe.config.impl; +import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -13,12 +15,22 @@ final class Path { final private String first; final private Path remainder; + // We only need to keep track of this for top-level paths created with + // parsePath, so this will be empty or null for all other cases + final private ArrayList<Token> tokens; + Path(String first, Path remainder) { + this(first, remainder, new ArrayList<Token>()); + } + + Path(String first, Path remainder, Collection<Token> tokens) { this.first = first; this.remainder = remainder; + this.tokens = new ArrayList<Token>(tokens); } Path(String... elements) { + this.tokens = null; if (elements.length == 0) throw new ConfigException.BugOrBroken("empty path"); this.first = elements[0]; @@ -40,6 +52,7 @@ final class Path { // append all the paths in the iterator together into one path Path(Iterator<Path> i) { + this.tokens = null; if (!i.hasNext()) throw new ConfigException.BugOrBroken("empty path"); @@ -204,6 +217,10 @@ final class Path { } } + protected Collection<Token> tokens() { + return tokens; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/config/src/main/java/com/typesafe/config/impl/PathBuilder.java b/config/src/main/java/com/typesafe/config/impl/PathBuilder.java index ede6c663..bd6b162e 100644 --- a/config/src/main/java/com/typesafe/config/impl/PathBuilder.java +++ b/config/src/main/java/com/typesafe/config/impl/PathBuilder.java @@ -3,6 +3,8 @@ */ package com.typesafe.config.impl; +import java.util.ArrayList; +import java.util.Collection; import java.util.Stack; import com.typesafe.config.ConfigException; @@ -12,8 +14,17 @@ final class PathBuilder { final private Stack<String> keys; private Path result; + // the tokens only matter for top-level paths created with parsePath, in all + // other cases this will be empty + final private ArrayList<Token> tokens; + PathBuilder() { + this(new ArrayList<Token>()); + } + + PathBuilder(Collection<Token> tokens) { keys = new Stack<String>(); + this.tokens = new ArrayList<Token>(tokens); } private void checkCanAppend() { @@ -51,7 +62,7 @@ final class PathBuilder { Path remainder = null; while (!keys.isEmpty()) { String key = keys.pop(); - remainder = new Path(key, remainder); + remainder = new Path(key, remainder, tokens); } result = remainder; } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigNodeTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigNodeTest.scala index fc245091..f57ec3f7 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigNodeTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigNodeTest.scala @@ -11,9 +11,9 @@ class ConfigNodeTest extends TestUtils { assertEquals(node.render(), token.tokenText()) } - private def keyNodeTest(token: Token) { - val node = configNodeKey(token) - assertEquals(node.render(), token.tokenText()) + private def keyNodeTest(path: String) { + val node = configNodeKey(path) + assertEquals(path, node.render()) } private def simpleValueNodeTest(token: Token) { @@ -32,21 +32,21 @@ class ConfigNodeTest extends TestUtils { assertEquals(newValue.render(), newKeyValNode.value().render()) } - private def topLevelValueReplaceTest(value: AbstractConfigNodeValue, newValue: AbstractConfigNodeValue, key: Token = tokenString("foo")) { + private def topLevelValueReplaceTest(value: AbstractConfigNodeValue, newValue: AbstractConfigNodeValue, key: String = "foo") { val complexNodeChildren = List(nodeOpenBrace, nodeKeyValuePair(nodeWhitespace(" "), configNodeKey(key),value, nodeWhitespace(" ")), nodeCloseBrace) val complexNode = configNodeComplexValue(complexNodeChildren) - val newNode = complexNode.setValueOnPath(Path.newPath(key.tokenText()), newValue) - val origText = "{ " + key.tokenText() + " : " + value.render() + " }" - val finalText = "{ " + key.tokenText() + " : " + newValue.render() + " }" + val newNode = complexNode.setValueOnPath(Path.newPath(key), newValue) + val origText = "{ " + key + " : " + value.render() + " }" + val finalText = "{ " + key + " : " + newValue.render() + " }" assertEquals(origText, complexNode.render()) assertEquals(finalText, newNode.render()) } private def replaceDuplicatesTest(value1: AbstractConfigNodeValue, value2: AbstractConfigNodeValue, value3: AbstractConfigNodeValue) { - val key = nodeUnquotedKey("foo") + val key = configNodeKey("foo") val keyValPair1 = nodeKeyValuePair(key, value1) val keyValPair2 = nodeKeyValuePair(key, value2) val keyValPair3 = nodeKeyValuePair(key, value3) @@ -59,7 +59,7 @@ class ConfigNodeTest extends TestUtils { } private def nonExistentPathTest(value: AbstractConfigNodeValue) { - val node = configNodeComplexValue(List(nodeKeyValuePair(nodeUnquotedKey("bar"), nodeInt(15)))) + val node = configNodeComplexValue(List(nodeKeyValuePair(configNodeKey("bar"), nodeInt(15)))) assertEquals("bar : 15", node.render()) val newNode = node.setValueOnPath(Path.newPath("foo"), value) val finalText = "bar : 15\nfoo : " + value.render() + "\n" @@ -89,8 +89,8 @@ class ConfigNodeTest extends TestUtils { @Test def createConfigNodeSetting() { //Ensure a ConfigNodeSetting can handle the normal key types - keyNodeTest(tokenUnquoted("foo")) - keyNodeTest(tokenString("Hello I am a key how are you today")) + keyNodeTest("foo") + keyNodeTest("\"Hello I am a key how are you today\"") } @Test @@ -112,12 +112,12 @@ class ConfigNodeTest extends TestUtils { @Test def createConfigNodeKeyValue() { // Supports Quoted and Unquoted keys - keyValueNodeTest(nodeQuotedKey("abc"), nodeInt(123), nodeLine(1), nodeInt(245)) - keyValueNodeTest(nodeUnquotedKey("abc"), nodeInt(123), nodeLine(1), nodeInt(245)) + keyValueNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeInt(245)) + keyValueNodeTest(configNodeKey("abc"), nodeInt(123), nodeLine(1), nodeInt(245)) // Can replace value with values of different types - keyValueNodeTest(nodeQuotedKey("abc"), nodeInt(123), nodeLine(1), nodeString("I am a string")) - keyValueNodeTest(nodeQuotedKey("abc"), nodeInt(123), nodeLine(1), configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace))) + keyValueNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeString("I am a string")) + keyValueNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace))) } @Test @@ -142,7 +142,7 @@ class ConfigNodeTest extends TestUtils { topLevelValueReplaceTest(array, configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace))) //Ensure maps can be replaced - val nestedMap = configNodeComplexValue(List(nodeOpenBrace, nodeUnquotedKey("abc"), + val nestedMap = configNodeComplexValue(List(nodeOpenBrace, configNodeKey("abc"), nodeColon, configNodeSimpleValue(tokenString("a string")), nodeCloseBrace)) topLevelValueReplaceTest(nestedMap, nodeInt(10)) @@ -152,7 +152,7 @@ class ConfigNodeTest extends TestUtils { topLevelValueReplaceTest(nestedMap, configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace))) //Ensure a key with format "a.b" will be properly replaced - topLevelValueReplaceTest(nodeInt(10), nestedMap, tokenUnquoted("foo.bar")) + topLevelValueReplaceTest(nodeInt(10), nestedMap, "foo.bar") } @Test @@ -170,7 +170,7 @@ class ConfigNodeTest extends TestUtils { def addNonExistentPaths() { nonExistentPathTest(nodeInt(10)) nonExistentPathTest(configNodeComplexValue(List(nodeOpenBracket, nodeInt(15), nodeCloseBracket))) - nonExistentPathTest(configNodeComplexValue(List(nodeOpenBrace, nodeKeyValuePair(nodeUnquotedKey("foo"), nodeDouble(3.14), nodeSpace)))) + nonExistentPathTest(configNodeComplexValue(List(nodeOpenBrace, nodeKeyValuePair(configNodeKey("foo"), nodeDouble(3.14), nodeSpace)))) } @Test @@ -179,23 +179,23 @@ class ConfigNodeTest extends TestUtils { val origText = "foo : bar\nbaz : {\n\t\"abc.def\" : 123\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" + "def : \"this is a string\"\n\t\tghi : ${\"a.b\"}\n\t}\n}\nbaz.abc.ghi : 52\nbaz.abc.ghi : 53\n}" val lowestLevelMap = configNodeComplexValue(List(nodeOpenBrace, nodeLine(6), - nodeKeyValuePair(nodeWhitespace("\t\t"), nodeUnquotedKey("def"), configNodeSimpleValue(tokenString("this is a string")), nodeLine(7)), - nodeKeyValuePair(nodeWhitespace("\t\t"), nodeUnquotedKey("ghi"), configNodeSimpleValue(tokenKeySubstitution("a.b")), nodeLine(8)), + nodeKeyValuePair(nodeWhitespace("\t\t"), configNodeKey("def"), configNodeSimpleValue(tokenString("this is a string")), nodeLine(7)), + nodeKeyValuePair(nodeWhitespace("\t\t"), configNodeKey("ghi"), configNodeSimpleValue(tokenKeySubstitution("a.b")), nodeLine(8)), nodeWhitespace("\t"), nodeCloseBrace)) val higherLevelMap = configNodeComplexValue(List(nodeOpenBrace, nodeLine(2), - nodeKeyValuePair(nodeWhitespace("\t"), configNodeKey(tokenString("abc.def")), configNodeSimpleValue(tokenInt(123)), nodeLine(3)), + nodeKeyValuePair(nodeWhitespace("\t"), configNodeKey("\"abc.def\""), configNodeSimpleValue(tokenInt(123)), nodeLine(3)), nodeWhitespace("\t"), configNodeBasic(tokenCommentDoubleSlash("This is a comment about the below setting")), nodeLine(4), nodeLine(5), - nodeKeyValuePair(nodeWhitespace("\t"), nodeUnquotedKey("abc"), lowestLevelMap, nodeLine(9)), nodeWhitespace(""), + nodeKeyValuePair(nodeWhitespace("\t"), configNodeKey("abc"), lowestLevelMap, nodeLine(9)), nodeWhitespace(""), nodeCloseBrace)) - val origNode = configNodeComplexValue(List(nodeKeyValuePair(nodeUnquotedKey("foo"), configNodeSimpleValue(tokenUnquoted("bar")), nodeLine(1)), - nodeKeyValuePair(nodeUnquotedKey("baz"), higherLevelMap, nodeLine(10)), - nodeKeyValuePair(nodeUnquotedKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(52)), nodeLine(11)), - nodeKeyValuePair(nodeUnquotedKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(53)), nodeLine(12)), + val origNode = configNodeComplexValue(List(nodeKeyValuePair(configNodeKey("foo"), configNodeSimpleValue(tokenUnquoted("bar")), nodeLine(1)), + nodeKeyValuePair(configNodeKey("baz"), higherLevelMap, nodeLine(10)), + nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(52)), nodeLine(11)), + nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(53)), nodeLine(12)), nodeCloseBrace)) assertEquals(origText, origNode.render()) val finalText = "foo : bar\nbaz : {\n\t\"abc.def\" : true\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" + - "def : false\n\t}\n}\nbaz.abc.ghi : randomunquotedString\n}\nbaz.abc.this.does.not.exist : doesnotexist\n" + "def : false\n\t}\n}\nbaz.abc.ghi : randomunquotedString\n}\nbaz.abc.\"this.does.not.exist@@@+$#\".end : doesnotexist\n" //Can replace settings in nested maps // Paths with quotes in the name are treated as a single Path, rather than multiple sub-paths @@ -206,7 +206,7 @@ class ConfigNodeTest extends TestUtils { newNode = newNode.setValueOnPath(Path.newPath("baz.abc.ghi"), configNodeSimpleValue(tokenUnquoted("randomunquotedString"))) // Missing paths are added to the top level if they don't appear anywhere, including in nested maps - newNode = newNode.setValueOnPath(Path.newPath("baz.abc.this.does.not.exist"), configNodeSimpleValue(tokenUnquoted("doesnotexist"))) + newNode = newNode.setValueOnPath(Path.newPath("baz.abc.\"this.does.not.exist@@@+$#\".end"), configNodeSimpleValue(tokenUnquoted("doesnotexist"))) // The above operations cause the resultant map to be rendered properly assertEquals(finalText, newNode.render()) diff --git a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala index 50ede888..3d7cfcb4 100644 --- a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala +++ b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala @@ -667,8 +667,9 @@ abstract trait TestUtils { new ConfigNodeSimpleValue(value) } - def configNodeKey(value: Token) = { - new ConfigNodeKey(value) + def configNodeKey(path: String) = { + val parsedPath = Parser.parsePath(path) + new ConfigNodeKey(parsedPath) } def configNodeBasic(value: Token) = { @@ -688,8 +689,6 @@ abstract trait TestUtils { def nodeComma = new ConfigNodeSingleToken(Tokens.COMMA) def nodeLine(line: Integer) = new ConfigNodeSingleToken(tokenLine(line)) def nodeWhitespace(whitespace: String) = new ConfigNodeSingleToken(tokenWhitespace(whitespace)) - def nodeQuotedKey(key: String) = configNodeKey(tokenString(key)) - def nodeUnquotedKey(key: String) = configNodeKey(tokenUnquoted(key)) def nodeKeyValuePair(key: ConfigNodeKey, value: AbstractConfigNodeValue) = { val nodes = List(key, nodeSpace, nodeColon, nodeSpace, value) new ConfigNodeKeyValue(nodes.asJavaCollection)