mirror of
https://github.com/lightbend/config.git
synced 2025-01-28 21:20:07 +08:00
Merge pull request #294 from fpringvaldsen/task/more-ConfigDocument-methods
Add new ConfigDocument API methods
This commit is contained in:
commit
ed004d47e7
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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