Merge pull request #294 from fpringvaldsen/task/more-ConfigDocument-methods

Add new ConfigDocument API methods
This commit is contained in:
Havoc Pennington 2015-03-30 16:18:56 -04:00
commit ed004d47e7
6 changed files with 181 additions and 34 deletions

View File

@ -1,41 +1,86 @@
package com.typesafe.config.impl;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigSyntax;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
final class ConfigNodeObject extends ConfigNodeComplexValue {
ConfigNodeObject(Collection<AbstractConfigNode> children) {
super(children);
}
protected ConfigNodeObject changeValueOnPath(Path desiredPath, AbstractConfigNodeValue value) {
public boolean hasValue(Path desiredPath) {
for (AbstractConfigNode node : children) {
if (node instanceof ConfigNodeField) {
ConfigNodeField field = (ConfigNodeField) node;
Path key = field.path().value();
if (key.equals(desiredPath) || key.startsWith(desiredPath)) {
return true;
} else if (desiredPath.startsWith(key)) {
if (field.value() instanceof ConfigNodeObject) {
ConfigNodeObject obj = (ConfigNodeObject) field.value();
Path remainingPath = desiredPath.subPath(key.length());
if (obj.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)
childrenCopy.remove(i);
else {
childrenCopy.set(i, node.replaceValue(value));
valueCopy = null;
// Delete all multi-element paths that start with the desired path, since technically they are duplicates
if ((valueCopy == null && key.equals(desiredPath))|| (key.startsWith(desiredPath) && !key.equals(desiredPath))) {
childrenCopy.remove(i);
// 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;
}
}
return new ConfigNodeObject(childrenCopy);
@ -51,11 +96,11 @@ 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())) {
return addValueOnPath(desiredPath, value, flavor);
if (!node.hasValue(desiredPath.value())) {
return node.addValueOnPath(desiredPath, value, flavor);
}
return node;
}
@ -120,8 +165,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

@ -27,7 +27,7 @@ class ConfigDocumentTest extends TestUtils {
}
@Test
def configDocumentReplace() {
def configDocumentReplace {
// Can handle parsing/replacement with a very simple map
configDocumentReplaceConfTest("""{"a":1}""", """{"a":2}""", "2", "a")
configDocumentReplaceJsonTest("""{"a":1}""", """{"a":2}""", "2", "a")
@ -136,6 +136,21 @@ class ConfigDocumentTest extends TestUtils {
"this is a concatenation 123 456 {a:b} [1,2,3] {a: this is another 123 concatenation null true}", "h.b.a")
}
@Test
def configDocumentMultiElementDuplicatesRemoved {
var origText = "{a: b, a.b.c: d, a: e}"
var configDoc = ConfigDocumentFactory.parseString(origText)
assertEquals("{a: 2}", configDoc.setValue("a", "2").render())
origText = "{a: b, a: e, a.b.c: d}"
configDoc = ConfigDocumentFactory.parseString(origText)
assertEquals("{a: 2, }", configDoc.setValue("a", "2").render())
origText = "{a.b.c: d}"
configDoc = ConfigDocumentFactory.parseString(origText)
assertEquals("{\na : 2\n}", configDoc.setValue("a", "2").render())
}
@Test
def configDocumentSetNewValueBraceRoot {
val origText = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\"\n}"
@ -180,20 +195,51 @@ 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)
var exceptionThrown = false
try {
document.setValue("a", "1")
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("ConfigDocument had an array at the root level"))
}
assertTrue(exceptionThrown)
val e1 = intercept[ConfigException] { document.setValue("a", "1") }
assertTrue(e1.getMessage.contains("ConfigDocument had an array at the root level"))
val e2 = intercept[ConfigException] { document.hasValue("a") }
assertTrue(e2.getMessage.contains("ConfigDocument had an array at the root level"))
val e3 = intercept[ConfigException] { document.removeValue("a") }
assertTrue(e3.getMessage.contains("ConfigDocument had an array at the root level"))
}
@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