Add new ConfigDocument API methods

Add two new methods to the ConfigDocument API, hasValue() and
removeValue(), along with tests.
This commit is contained in:
Preben Ingvaldsen 2015-03-27 15:32:08 -07:00
parent d4ab52fb6b
commit 97bd1f60c0
6 changed files with 178 additions and 17 deletions

View File

@ -12,32 +12,83 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
super(children);
}
protected ConfigNodeObject changeValueOnPath(Path desiredPath, AbstractConfigNodeValue value) {
public boolean hasValue(Path desiredPath) {
for (AbstractConfigNode node : children) {
if (node instanceof ConfigNodeField) {
Path key = ((ConfigNodeField) node).path().value();
if (key.equals(desiredPath) || key.startsWith(desiredPath)) {
return true;
} else if (desiredPath.startsWith(key)) {
if (((ConfigNodeField) node).value() instanceof ConfigNodeObject) {
Path remainingPath = desiredPath.subPath(key.length());
if (((ConfigNodeObject) ((ConfigNodeField) node).value()).hasValue(remainingPath)) {
return true;
}
}
}
}
}
return false;
}
protected ConfigNodeObject changeValueOnPath(Path desiredPath, AbstractConfigNodeValue value, ConfigSyntax flavor) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList<AbstractConfigNode>(super.children);
boolean seenNonMatching = false;
// 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)) {
for (int i = childrenCopy.size() - 1; i >= 0; i--) {
if (childrenCopy.get(i) instanceof ConfigNodeSingleToken) {
Token t = ((ConfigNodeSingleToken) childrenCopy.get(i)).token();
// Ensure that, when we are removing settings in JSON, we don't end up with a trailing comma
if (flavor == ConfigSyntax.JSON && !seenNonMatching && t == Tokens.COMMA) {
childrenCopy.remove(i);
}
continue;
}else if (!(childrenCopy.get(i) instanceof ConfigNodeField)) {
continue;
}
ConfigNodeField node = (ConfigNodeField)super.children.get(i);
ConfigNodeField node = (ConfigNodeField)childrenCopy.get(i);
Path key = node.path().value();
if (key.equals(desiredPath)) {
if (valueCopy == null)
if (key.equals(desiredPath) || key.startsWith(desiredPath)) {
if (valueCopy == null) {
childrenCopy.remove(i);
else {
// Remove any whitespace or commas after the deleted setting
for (int j = i; j < childrenCopy.size(); j++) {
if (childrenCopy.get(j) instanceof ConfigNodeSingleToken) {
Token t = ((ConfigNodeSingleToken) childrenCopy.get(j)).token();
if (Tokens.isIgnoredWhitespace(t) || t == Tokens.COMMA) {
childrenCopy.remove(j);
j--;
} else {
break;
}
} else {
break;
}
}
}
else if (key.equals(desiredPath)){
seenNonMatching = true;
childrenCopy.set(i, node.replaceValue(value));
valueCopy = null;
}
} else if (desiredPath.startsWith(key)) {
seenNonMatching = true;
if (node.value() instanceof ConfigNodeObject) {
Path remainingPath = desiredPath.subPath(key.length());
childrenCopy.set(i, node.replaceValue(((ConfigNodeObject)node.value()).changeValueOnPath(remainingPath, valueCopy)));
childrenCopy.set(i, node.replaceValue(((ConfigNodeObject)node.value()).changeValueOnPath(remainingPath, valueCopy, flavor)));
if (valueCopy != null && !node.equals(super.children.get(i)))
valueCopy = null;
}
} else {
seenNonMatching = true;
}
}
// Since we've removed values and valid JSON does not allow trailing commas, remove a comma after the final setting
if (flavor == ConfigSyntax.JSON) {
}
return new ConfigNodeObject(childrenCopy);
}
@ -51,10 +102,10 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
}
private ConfigNodeObject setValueOnPath(ConfigNodePath desiredPath, AbstractConfigNodeValue value, ConfigSyntax flavor) {
ConfigNodeObject node = changeValueOnPath(desiredPath.value(), value);
ConfigNodeObject node = changeValueOnPath(desiredPath.value(), value, flavor);
// If the desired Path did not exist, add it
if (node.render().equals(render())) {
if (node.equals(this)) {
return addValueOnPath(desiredPath, value, flavor);
}
return node;
@ -120,8 +171,8 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
return new ConfigNodeObject(childrenCopy);
}
public ConfigNodeComplexValue removeValueOnPath(String desiredPath) {
Path path = PathParser.parsePath(desiredPath);
return changeValueOnPath(path, null);
public ConfigNodeObject removeValueOnPath(String desiredPath, ConfigSyntax flavor) {
Path path = PathParser.parsePathNode(desiredPath, flavor).value();
return changeValueOnPath(path, null, flavor);
}
}

View File

@ -30,13 +30,33 @@ final class ConfigNodeRoot extends ConfigNodeComplexValue {
AbstractConfigNode node = childrenCopy.get(i);
if (node instanceof ConfigNodeComplexValue) {
if (node instanceof ConfigNodeArray) {
throw new ConfigException.WrongType(origin, "The ConfigDocument had an array at the root level, and values cannot be replaced inside an array.");
throw new ConfigException.WrongType(origin, "The ConfigDocument had an array at the root level, and values cannot be modified inside an array.");
} else if (node instanceof ConfigNodeObject) {
childrenCopy.set(i, ((ConfigNodeObject) node).setValueOnPath(desiredPath, value, flavor));
if (value == null) {
childrenCopy.set(i, ((ConfigNodeObject)node).removeValueOnPath(desiredPath, flavor));
} else {
childrenCopy.set(i, ((ConfigNodeObject) node).setValueOnPath(desiredPath, value, flavor));
}
return new ConfigNodeRoot(childrenCopy, origin);
}
}
}
throw new ConfigException.BugOrBroken("ConfigNodeRoot did not contain a value");
}
protected boolean hasValue(String desiredPath) {
Path path = PathParser.parsePath(desiredPath);
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList<AbstractConfigNode>(children);
for (int i = 0; i < childrenCopy.size(); i++) {
AbstractConfigNode node = childrenCopy.get(i);
if (node instanceof ConfigNodeComplexValue) {
if (node instanceof ConfigNodeArray) {
throw new ConfigException.WrongType(origin, "The ConfigDocument had an array at the root level, and values cannot be modified inside an array.");
} else if (node instanceof ConfigNodeObject) {
return ((ConfigNodeObject) node).hasValue(path);
}
}
}
throw new ConfigException.BugOrBroken("ConfigNodeRoot did not contain a value");
}
}

View File

@ -31,7 +31,25 @@ final class SimpleConfigDocument implements ConfigDocument {
return setValue(path, newValue.render());
}
public ConfigDocument removeValue(String path) {
return new SimpleConfigDocument(configNodeTree.setValue(path, null, parseOptions.getSyntax()), parseOptions);
}
public boolean hasValue(String path) {
return configNodeTree.hasValue(path);
}
public String render() {
return configNodeTree.render();
}
@Override
public boolean equals(Object other) {
return other instanceof ConfigDocument && render().equals(((ConfigDocument) other).render());
}
@Override
public int hashCode() {
return render().hashCode();
}
}

View File

@ -54,6 +54,24 @@ public interface ConfigDocument {
*/
ConfigDocument setValue(String path, ConfigValue newValue);
/**
* Returns a new ConfigDocument that is a copy of the current ConfigDocument, but with
* the value at the desired path removed. If the desired path does not exist in the document,
* a copy of the current document will be returned. If there is an array at the root, an exception
* will be thrown.
*
* @param path the path to remove from the document
* @return a copy of the ConfigDocument with the desired value removed from the document.
*/
ConfigDocument removeValue(String path);
/**
* Returns a boolean indicating whether or not a ConfigDocument has a value at the desired path.
* @param path the path to check
* @return true if the path exists in the document, otherwise false
*/
boolean hasValue(String path);
/**
* The original text of the input, modified if necessary with
* any replaced or added values.

View File

@ -180,7 +180,39 @@ class ConfigDocumentTest extends TestUtils {
}
@Test
def configDocumentArrayReplaceFailure {
def configDocumentHasValue {
val origText = "{a: b, a.b.c.d: e, c: {a: {b: c}}}"
val configDoc = ConfigDocumentFactory.parseString(origText)
assertTrue(configDoc.hasValue("a"))
assertTrue(configDoc.hasValue("a.b.c"))
assertTrue(configDoc.hasValue("c.a.b"))
assertFalse(configDoc.hasValue("c.a.b.c"))
assertFalse(configDoc.hasValue("a.b.c.d.e"))
assertFalse(configDoc.hasValue("this.does.not.exist"))
}
@Test
def configDocumentRemoveValue {
val origText = "{a: b, a.b.c.d: e, c: {a: {b: c}}}"
val configDoc = ConfigDocumentFactory.parseString(origText)
assertEquals("{c: {a: {b: c}}}", configDoc.removeValue("a").render())
assertEquals("{a: b, a.b.c.d: e, }", configDoc.removeValue("c").render())
assertEquals(configDoc, configDoc.removeValue("this.does.not.exist"))
}
@Test
def configDocumentRemoveValueJSON {
val origText = """{"a": "b", "c": "d"}"""
val configDoc = ConfigDocumentFactory.parseString(origText, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
// Ensure that removing a value in JSON does not leave us with a trailing comma
assertEquals("""{"a": "b" }""", configDoc.removeValue("c").render())
}
@Test
def configDocumentArrayFailures {
// Attempting a replace on a ConfigDocument parsed from an array throws an error
val origText = "[1, 2, 3, 4, 5]"
val document = ConfigDocumentFactory.parseString(origText)
@ -194,6 +226,28 @@ class ConfigDocumentTest extends TestUtils {
assertTrue(e.getMessage.contains("ConfigDocument had an array at the root level"))
}
assertTrue(exceptionThrown)
exceptionThrown = false;
try {
document.hasValue("a")
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("ConfigDocument had an array at the root level"))
}
assertTrue(exceptionThrown)
exceptionThrown = false
try {
document.removeValue("a")
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("ConfigDocument had an array at the root level"))
}
assertTrue(exceptionThrown)
}
@Test

View File

@ -212,7 +212,7 @@ class ConfigNodeTest extends TestUtils {
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\"this.does.not.exist@@@+$#\" : {\nend : doesnotexist\n}\n}\n}\nbaz.abc.ghi : randomunquotedString\n}"
"def : false\n\n\"this.does.not.exist@@@+$#\" : {\nend : doesnotexist\n}\n}\n}\nbaz.abc.ghi : randomunquotedString\n}"
//Can replace settings in nested maps
// Paths with quotes in the name are treated as a single Path, rather than multiple sub-paths