Add new subclasses of ConfigNodeComplexValue

Add three new classes, ConfigNodeObject, ConfigNodeArray, and
ConfigNodeConcatenation, to differentiate the three types of
complex nodes. Disallow setting values outside of
ConfigNodeObjects.
This commit is contained in:
Preben Ingvaldsen 2015-03-18 10:35:53 -07:00
parent d3b33cc6c2
commit c44ef1c6f7
7 changed files with 150 additions and 102 deletions

View File

@ -178,7 +178,7 @@ final class ConfigDocumentParser {
return value;
}
return new ConfigNodeComplexValue(values);
return new ConfigNodeConcatenation(values);
}
private ConfigException parseError(String message) {
@ -500,7 +500,7 @@ final class ConfigDocumentParser {
}
}
return new ConfigNodeComplexValue(objectNodes);
return new ConfigNodeObject(objectNodes);
}
private ConfigNodeComplexValue parseArray() {
@ -521,7 +521,7 @@ final class ConfigDocumentParser {
if (t == Tokens.CLOSE_SQUARE) {
arrayCount -= 1;
children.add(new ConfigNodeSingleToken(t));
return new ConfigNodeComplexValue(children);
return new ConfigNodeArray(children);
} else if (Tokens.isValue(t) || t == Tokens.OPEN_CURLY
|| t == Tokens.OPEN_SQUARE || Tokens.isUnquotedText(t)
|| Tokens.isSubstitution(t)) {
@ -547,7 +547,7 @@ final class ConfigDocumentParser {
if (t == Tokens.CLOSE_SQUARE) {
arrayCount -= 1;
children.add(new ConfigNodeSingleToken(t));
return new ConfigNodeComplexValue(children);
return new ConfigNodeArray(children);
} else {
throw parseError(addKeyName("List should have ended with ] or had a comma, instead had token: "
+ t
@ -616,7 +616,10 @@ final class ConfigDocumentParser {
ArrayList<AbstractConfigNode> children = new ArrayList<AbstractConfigNode>(((ConfigNodeComplexValue)result).children());
t = nextTokenIgnoringWhitespace(children);
if (t == Tokens.END) {
return new ConfigNodeComplexValue(children);
if (result instanceof ConfigNodeArray) {
return new ConfigNodeArray(children);
}
return new ConfigNodeObject(children);
} else {
throw parseError("Document has trailing tokens after first object or array: "
+ t);

View File

@ -0,0 +1,9 @@
package com.typesafe.config.impl;
import java.util.Collection;
final class ConfigNodeArray extends ConfigNodeComplexValue {
ConfigNodeArray(Collection<AbstractConfigNode> children) {
super(children);
}
}

View File

@ -5,8 +5,8 @@ package com.typesafe.config.impl;
import java.util.*;
final class ConfigNodeComplexValue extends AbstractConfigNodeValue {
final private ArrayList<AbstractConfigNode> children;
abstract class ConfigNodeComplexValue extends AbstractConfigNodeValue {
final protected ArrayList<AbstractConfigNode> children;
ConfigNodeComplexValue(Collection<AbstractConfigNode> children) {
this.children = new ArrayList(children);
@ -24,64 +24,4 @@ final class ConfigNodeComplexValue extends AbstractConfigNodeValue {
}
return tokens;
}
protected ConfigNodeComplexValue changeValueOnPath(Path desiredPath, AbstractConfigNodeValue value) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList(children);
// Copy the value so we can change it to null but not modify the original parameter
AbstractConfigNodeValue valueCopy = value;
for (int i = children.size() - 1; i >= 0; i--) {
if (!(children.get(i) instanceof ConfigNodeField)) {
continue;
}
ConfigNodeField node = (ConfigNodeField)children.get(i);
Path key = node.path().value();
if (key.equals(desiredPath)) {
if (valueCopy == null)
childrenCopy.remove(i);
else {
childrenCopy.set(i, node.replaceValue(value));
valueCopy = null;
}
} else if (desiredPath.startsWith(key)) {
if (node.value() instanceof ConfigNodeComplexValue) {
Path remainingPath = desiredPath.subPath(key.length());
childrenCopy.set(i, node.replaceValue(((ConfigNodeComplexValue) node.value()).changeValueOnPath(remainingPath, valueCopy)));
if (valueCopy != null && node.render() != children.get(i).render())
valueCopy = null;
}
}
}
return new ConfigNodeComplexValue(childrenCopy);
}
public ConfigNodeComplexValue setValueOnPath(String desiredPath, AbstractConfigNodeValue value) {
ConfigNodePath path = PathParser.parsePathNode(desiredPath);
return setValueOnPath(path, value);
}
private ConfigNodeComplexValue setValueOnPath(ConfigNodePath desiredPath, AbstractConfigNodeValue value) {
ConfigNodeComplexValue node = changeValueOnPath(desiredPath.value(), value);
// If the desired Path did not exist, add it
if (node.render().equals(render())) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList<AbstractConfigNode>(children);
ArrayList<AbstractConfigNode> newNodes = new ArrayList();
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
newNodes.add(desiredPath);
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newNodes.add(new ConfigNodeSingleToken(Tokens.COLON));
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newNodes.add(value);
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
childrenCopy.add(new ConfigNodeField(newNodes));
node = new ConfigNodeComplexValue(childrenCopy);
}
return node;
}
public ConfigNodeComplexValue removeValueOnPath(String desiredPath) {
Path path = PathParser.parsePath(desiredPath);
return changeValueOnPath(path, null);
}
}

View File

@ -0,0 +1,9 @@
package com.typesafe.config.impl;
import java.util.Collection;
final class ConfigNodeConcatenation extends ConfigNodeComplexValue {
ConfigNodeConcatenation(Collection<AbstractConfigNode> children) {
super(children);
}
}

View File

@ -0,0 +1,69 @@
package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Collection;
final class ConfigNodeObject extends ConfigNodeComplexValue {
ConfigNodeObject(Collection<AbstractConfigNode> children) {
super(children);
}
protected ConfigNodeObject changeValueOnPath(Path desiredPath, AbstractConfigNodeValue value) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList(super.children);
// Copy the value so we can change it to null but not modify the original parameter
AbstractConfigNodeValue valueCopy = value;
for (int i = super.children.size() - 1; i >= 0; i--) {
if (!(super.children.get(i) instanceof ConfigNodeField)) {
continue;
}
ConfigNodeField node = (ConfigNodeField)super.children.get(i);
Path key = node.path().value();
if (key.equals(desiredPath)) {
if (valueCopy == null)
childrenCopy.remove(i);
else {
childrenCopy.set(i, node.replaceValue(value));
valueCopy = null;
}
} else if (desiredPath.startsWith(key)) {
if (node.value() instanceof ConfigNodeObject) {
Path remainingPath = desiredPath.subPath(key.length());
childrenCopy.set(i, node.replaceValue(((ConfigNodeObject)node.value()).changeValueOnPath(remainingPath, valueCopy)));
if (valueCopy != null && node.render() != super.children.get(i).render())
valueCopy = null;
}
}
}
return new ConfigNodeObject(childrenCopy);
}
public ConfigNodeObject setValueOnPath(String desiredPath, AbstractConfigNodeValue value) {
ConfigNodePath path = PathParser.parsePathNode(desiredPath);
return setValueOnPath(path, value);
}
private ConfigNodeObject setValueOnPath(ConfigNodePath desiredPath, AbstractConfigNodeValue value) {
ConfigNodeObject node = changeValueOnPath(desiredPath.value(), value);
// If the desired Path did not exist, add it
if (node.render().equals(render())) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList<AbstractConfigNode>(super.children);
ArrayList<AbstractConfigNode> newNodes = new ArrayList();
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
newNodes.add(desiredPath);
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newNodes.add(new ConfigNodeSingleToken(Tokens.COLON));
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newNodes.add(value);
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
childrenCopy.add(new ConfigNodeField(newNodes));
node = new ConfigNodeObject(childrenCopy);
}
return node;
}
public ConfigNodeComplexValue removeValueOnPath(String desiredPath) {
Path path = PathParser.parsePath(desiredPath);
return changeValueOnPath(path, null);
}
}

View File

@ -20,7 +20,7 @@ class ConfigNodeTest extends TestUtils {
assertEquals(node.render(), token.tokenText())
}
private def keyValueNodeTest(key: ConfigNodePath, value: AbstractConfigNodeValue, trailingWhitespace: ConfigNodeSingleToken, newValue: AbstractConfigNodeValue) {
private def fieldNodeTest(key: ConfigNodePath, value: AbstractConfigNodeValue, trailingWhitespace: ConfigNodeSingleToken, newValue: AbstractConfigNodeValue) {
val keyValNode = nodeKeyValuePair(key, value, trailingWhitespace)
assertEquals(key.render() + " : " + value.render() + trailingWhitespace.render(), keyValNode.render())
assertEquals(key.render, keyValNode.path().render())
@ -35,7 +35,7 @@ class ConfigNodeTest extends TestUtils {
val complexNodeChildren = List(nodeOpenBrace,
nodeKeyValuePair(nodeWhitespace(" "), configNodeKey(key),value, nodeWhitespace(" ")),
nodeCloseBrace)
val complexNode = configNodeComplexValue(complexNodeChildren)
val complexNode = configNodeObject(complexNodeChildren)
val newNode = complexNode.setValueOnPath(key, newValue)
val origText = "{ " + key + " : " + value.render() + " }"
val finalText = "{ " + key + " : " + newValue.render() + " }"
@ -49,7 +49,7 @@ class ConfigNodeTest extends TestUtils {
val keyValPair1 = nodeKeyValuePair(key, value1)
val keyValPair2 = nodeKeyValuePair(key, value2)
val keyValPair3 = nodeKeyValuePair(key, value3)
val complexNode = configNodeComplexValue(List(keyValPair1, keyValPair2, keyValPair3))
val complexNode = configNodeObject(List(keyValPair1, keyValPair2, keyValPair3))
val origText = keyValPair1.render() + keyValPair2.render() + keyValPair3.render()
val finalText = key.render() + " : 15"
@ -58,7 +58,7 @@ class ConfigNodeTest extends TestUtils {
}
private def nonExistentPathTest(value: AbstractConfigNodeValue) {
val node = configNodeComplexValue(List(nodeKeyValuePair(configNodeKey("bar"), nodeInt(15))))
val node = configNodeObject(List(nodeKeyValuePair(configNodeKey("bar"), nodeInt(15))))
assertEquals("bar : 15", node.render())
val newNode = node.setValueOnPath("foo", value)
val finalText = "bar : 15\nfoo : " + value.render() + "\n"
@ -109,14 +109,14 @@ class ConfigNodeTest extends TestUtils {
}
@Test
def createConfigNodeKeyValue() {
def createConfigNodeField() {
// Supports Quoted and Unquoted keys
keyValueNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeInt(245))
keyValueNodeTest(configNodeKey("abc"), nodeInt(123), nodeLine(1), nodeInt(245))
fieldNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeInt(245))
fieldNodeTest(configNodeKey("abc"), nodeInt(123), nodeLine(1), nodeInt(245))
// Can replace value with values of different types
keyValueNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeString("I am a string"))
keyValueNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace)))
fieldNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeString("I am a string"))
fieldNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), configNodeObject(List(nodeOpenBrace, nodeCloseBrace)))
}
@Test
@ -135,20 +135,30 @@ class ConfigNodeTest extends TestUtils {
topLevelValueReplaceTest(nodeSubstitution(tokenUnquoted("a.b")), nodeInt(10))
// Ensure arrays can be replaced
val array = configNodeComplexValue(List(nodeOpenBracket, nodeInt(10), nodeSpace, nodeComma, nodeSpace, nodeInt(15), nodeCloseBracket))
val array = configNodeArray(List(nodeOpenBracket, nodeInt(10), nodeSpace, nodeComma, nodeSpace, nodeInt(15), nodeCloseBracket))
topLevelValueReplaceTest(nodeInt(10), array)
topLevelValueReplaceTest(array, nodeInt(10))
topLevelValueReplaceTest(array, configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace)))
topLevelValueReplaceTest(array, configNodeObject(List(nodeOpenBrace, nodeCloseBrace)))
//Ensure maps can be replaced
val nestedMap = configNodeComplexValue(List(nodeOpenBrace, configNodeKey("abc"),
// Ensure maps can be replaced
val nestedMap = configNodeObject(List(nodeOpenBrace, configNodeKey("abc"),
nodeColon, configNodeSimpleValue(tokenString("a string")),
nodeCloseBrace))
topLevelValueReplaceTest(nestedMap, nodeInt(10))
topLevelValueReplaceTest(nodeInt(10), nestedMap)
topLevelValueReplaceTest(array, nestedMap)
topLevelValueReplaceTest(nestedMap, array)
topLevelValueReplaceTest(nestedMap, configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace)))
topLevelValueReplaceTest(nestedMap, configNodeObject(List(nodeOpenBrace, nodeCloseBrace)))
// Ensure concatenations can be replaced
val concatenation = configNodeConcatenation(List(nodeInt(10), nodeSpace, nodeString("Hello")))
topLevelValueReplaceTest(concatenation, nodeInt(12))
topLevelValueReplaceTest(nodeInt(12), concatenation)
topLevelValueReplaceTest(nestedMap, concatenation)
topLevelValueReplaceTest(concatenation, nestedMap)
topLevelValueReplaceTest(array, concatenation)
topLevelValueReplaceTest(concatenation, array)
//Ensure a key with format "a.b" will be properly replaced
topLevelValueReplaceTest(nodeInt(10), nestedMap, "foo.bar")
@ -156,8 +166,8 @@ class ConfigNodeTest extends TestUtils {
@Test
def removeDuplicates() {
val emptyMapNode = configNodeComplexValue(List(nodeOpenBrace, nodeCloseBrace))
val emptyArrayNode = configNodeComplexValue(List(nodeOpenBracket, nodeCloseBracket))
val emptyMapNode = configNodeObject(List(nodeOpenBrace, nodeCloseBrace))
val emptyArrayNode = configNodeArray(List(nodeOpenBracket, nodeCloseBracket))
//Ensure duplicates of a key are removed from a map
replaceDuplicatesTest(nodeInt(10), nodeTrue, nodeNull)
replaceDuplicatesTest(emptyMapNode, emptyMapNode, emptyMapNode)
@ -168,8 +178,8 @@ class ConfigNodeTest extends TestUtils {
@Test
def addNonExistentPaths() {
nonExistentPathTest(nodeInt(10))
nonExistentPathTest(configNodeComplexValue(List(nodeOpenBracket, nodeInt(15), nodeCloseBracket)))
nonExistentPathTest(configNodeComplexValue(List(nodeOpenBrace, nodeKeyValuePair(configNodeKey("foo"), nodeDouble(3.14), nodeSpace))))
nonExistentPathTest(configNodeArray(List(nodeOpenBracket, nodeInt(15), nodeCloseBracket)))
nonExistentPathTest(configNodeObject(List(nodeOpenBrace, nodeKeyValuePair(configNodeKey("foo"), nodeDouble(3.14), nodeSpace))))
}
@Test
@ -177,21 +187,21 @@ class ConfigNodeTest extends TestUtils {
// Test that all features of node replacement in a map work in a complex map containing nested maps
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"), 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("\"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"), configNodeKey("abc"), lowestLevelMap, nodeLine(9)), nodeWhitespace(""),
nodeCloseBrace))
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))
val lowestLevelMap = configNodeObject(List(nodeOpenBrace, nodeLine(6),
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 = configNodeObject(List(nodeOpenBrace, nodeLine(2),
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"), configNodeKey("abc"), lowestLevelMap, nodeLine(9)), nodeWhitespace(""),
nodeCloseBrace))
val origNode = configNodeObject(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@@@+$#\".end : doesnotexist\n"

View File

@ -673,8 +673,16 @@ abstract trait TestUtils {
new ConfigNodeSingleToken(value: Token)
}
def configNodeComplexValue(nodes: List[AbstractConfigNode]) = {
new ConfigNodeComplexValue(nodes.asJavaCollection)
def configNodeObject(nodes: List[AbstractConfigNode]) = {
new ConfigNodeObject(nodes.asJavaCollection)
}
def configNodeArray(nodes: List[AbstractConfigNode]) = {
new ConfigNodeArray(nodes.asJavaCollection)
}
def configNodeConcatenation(nodes: List[AbstractConfigNode]) = {
new ConfigNodeConcatenation(nodes.asJavaCollection)
}
def nodeColon = new ConfigNodeSingleToken(Tokens.COLON)