mirror of
https://github.com/lightbend/config.git
synced 2025-03-26 12:06:02 +08:00
Add new ConfigDocument API methods
Add two new methods to the ConfigDocument API, hasValue() and removeValue(), along with tests.
This commit is contained in:
parent
d4ab52fb6b
commit
97bd1f60c0
config/src
main/java/com/typesafe/config
test/scala/com/typesafe/config/impl
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user