Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Config: addressing array elements via property paths #216
* subclass-based impl
- Loading branch information
Showing
12 changed files
with
373 additions
and
227 deletions.
There are no files selected for viewing
87 changes: 0 additions & 87 deletions
87
bootique/src/main/java/io/bootique/config/jackson/ArrayIndexPathSegment.java
This file was deleted.
Oops, something went wrong.
45 changes: 0 additions & 45 deletions
45
bootique/src/main/java/io/bootique/config/jackson/CiPathSegment.java
This file was deleted.
Oops, something went wrong.
52 changes: 52 additions & 0 deletions
52
bootique/src/main/java/io/bootique/config/jackson/CiPropertySegment.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package io.bootique.config.jackson; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
|
||
import java.util.Iterator; | ||
import java.util.Map.Entry; | ||
|
||
/** | ||
* A path segment for case-insensitive path. | ||
*/ | ||
class CiPropertySegment extends PropertySegment { | ||
|
||
protected CiPropertySegment(JsonNode node, PathSegment parent, String incomingPath, String remainingPath) { | ||
super(node, parent, incomingPath, remainingPath); | ||
} | ||
|
||
CiPropertySegment(JsonNode node, String remainingPath) { | ||
super(node, null, null, remainingPath); | ||
} | ||
|
||
@Override | ||
protected JsonNode readChild(String childName) { | ||
String key = getNode() != null ? getChildCiKey(getNode(), childName) : childName; | ||
return getNode() != null ? getNode().get(key) : null; | ||
} | ||
|
||
@Override | ||
protected PathSegment createIndexedChild(String childName, String remainingPath) { | ||
throw new UnsupportedOperationException("Indexed CI children are unsupported"); | ||
} | ||
|
||
@Override | ||
protected PathSegment createPropertyChild(String childName, String remainingPath) { | ||
return new CiPropertySegment(readChild(childName), this, childName, remainingPath); | ||
} | ||
|
||
private String getChildCiKey(JsonNode parent, String fieldName) { | ||
|
||
fieldName = fieldName.toUpperCase(); | ||
|
||
Iterator<Entry<String, JsonNode>> fields = parent.fields(); | ||
while (fields.hasNext()) { | ||
Entry<String, JsonNode> f = fields.next(); | ||
if (fieldName.equalsIgnoreCase(f.getKey())) { | ||
return f.getKey(); | ||
} | ||
} | ||
|
||
return fieldName; | ||
} | ||
|
||
} |
42 changes: 20 additions & 22 deletions
42
bootique/src/main/java/io/bootique/config/jackson/InPlaceMapOverrider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,41 @@ | ||
package io.bootique.config.jackson; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
|
||
import java.util.Map; | ||
import java.util.function.Function; | ||
|
||
/** | ||
* Overrides JsonNode object values from a map of properties. | ||
* | ||
* | ||
* @since 0.17 | ||
*/ | ||
public class InPlaceMapOverrider implements Function<JsonNode, JsonNode> { | ||
|
||
private Map<String, String> properties; | ||
private Map<String, String> properties; | ||
|
||
public InPlaceMapOverrider(Map<String, String> properties) { | ||
this.properties = properties; | ||
} | ||
public InPlaceMapOverrider(Map<String, String> properties) { | ||
this.properties = properties; | ||
} | ||
|
||
@Override | ||
public JsonNode apply(JsonNode t) { | ||
properties.entrySet().forEach(e -> { | ||
@Override | ||
public JsonNode apply(JsonNode t) { | ||
properties.entrySet().forEach(e -> { | ||
|
||
PathSegment target = lastPathComponent(t, e.getKey()); | ||
target.fillMissingParents(); | ||
PathSegment target = lastPathComponent(t, e.getKey()); | ||
target.fillMissingParents(); | ||
|
||
if (!(target.getParentNode() instanceof ObjectNode)) { | ||
throw new IllegalArgumentException("Invalid property '" + e.getKey() + "'"); | ||
} | ||
if (target.getParent() == null) { | ||
throw new IllegalArgumentException("No parent node"); | ||
} | ||
|
||
ObjectNode parentObjectNode = (ObjectNode) target.getParentNode(); | ||
parentObjectNode.put(target.getIncomingPath(), e.getValue()); | ||
}); | ||
target.getParent().writeChild(target.getIncomingPath(), e.getValue()); | ||
}); | ||
|
||
return t; | ||
} | ||
return t; | ||
} | ||
|
||
protected PathSegment lastPathComponent(JsonNode t, String path) { | ||
return new PathSegment(t, path).lastPathComponent().get(); | ||
} | ||
protected PathSegment lastPathComponent(JsonNode t, String path) { | ||
return PathSegment.create(t, path).lastPathComponent().get(); | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
bootique/src/main/java/io/bootique/config/jackson/IndexSegment.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package io.bootique.config.jackson; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.node.ArrayNode; | ||
import com.fasterxml.jackson.databind.node.JsonNodeFactory; | ||
|
||
/** | ||
* A path segment with remaining path being an array index. | ||
*/ | ||
class IndexSegment extends PathSegment { | ||
|
||
protected IndexSegment(JsonNode node, PathSegment parent, String incomingPath, String remainingPath) { | ||
super(node, parent, incomingPath, remainingPath); | ||
|
||
if (remainingPath != null && remainingPath.length() < 3) { | ||
|
||
if (remainingPath.length() < 3) { | ||
throw new IllegalArgumentException("The path must start with array index [NNN]. Instead got: " + remainingPath); | ||
} | ||
|
||
if (remainingPath.charAt(0) != ARRAY_INDEX_START) { | ||
throw new IllegalArgumentException("The path must start with array index [NNN]. Instead got: " + remainingPath); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
protected PathSegment parseNextNotEmpty(String path) { | ||
int len = path.length(); | ||
|
||
// looking for ']' or '].' | ||
// start at index 1.. The first char is known to be '[' | ||
for (int i = 1; i < len; i++) { | ||
char c = path.charAt(i); | ||
|
||
if (c == IndexSegment.ARRAY_INDEX_END) { | ||
|
||
// 1. [NNN] | ||
if (i == len - 1) { | ||
return createValueChild(path.substring(0, i + 1)); | ||
} | ||
// 2. [NNN].aaaa (i.e. in the second case the dot must follow closing paren) | ||
else if (path.charAt(i + 1) == PathSegment.DOT) { | ||
return createPropertyChild(path.substring(0, i + 1), path.substring(i + 2)); | ||
} | ||
// 3. [NNN][MMM] TODO => createIndexedChild | ||
// 4. Invalid path | ||
else { | ||
throw new IllegalStateException("Invalid path after array index: " + path); | ||
} | ||
} | ||
} | ||
|
||
throw new IllegalStateException("No closing array index parenthesis: " + path); | ||
} | ||
|
||
@Override | ||
protected void fillMissingNodes(String field, JsonNode child, JsonNodeFactory nodeFactory) { | ||
|
||
if (node == null || node.isNull()) { | ||
node = new ArrayNode(nodeFactory); | ||
parent.fillMissingNodes(incomingPath, node, nodeFactory); | ||
} | ||
|
||
if (child != null) { | ||
writeChild(field, child); | ||
} | ||
} | ||
|
||
@Override | ||
JsonNode readChild(String childName) { | ||
return node != null ? node.get(toIndex(childName)) : null; | ||
} | ||
|
||
@Override | ||
void writeChild(String childName, String value) { | ||
ArrayNode arrayNode = toArrayNode(); | ||
JsonNode childNode = value == null ? arrayNode.nullNode() : arrayNode.textNode(value); | ||
writeChild(childName, childNode); | ||
} | ||
|
||
private void writeChild(String childName, JsonNode childNode) { | ||
ArrayNode arrayNode = toArrayNode(); | ||
int index = toIndex(childName); | ||
|
||
// allow replacing elements at index | ||
if (index < arrayNode.size()) { | ||
arrayNode.set(index, childNode); | ||
} | ||
// allow appending elements to the end of the array... | ||
else if (index == arrayNode.size()) { | ||
arrayNode.add(childNode); | ||
} else { | ||
throw new ArrayIndexOutOfBoundsException("Array index out of bounds: " + index + ". Size: " + arrayNode.size()); | ||
} | ||
} | ||
|
||
protected int toIndex(String indexWithParenthesis) { | ||
|
||
if (indexWithParenthesis.length() < 3) { | ||
throw new IllegalArgumentException("Invalid array index. Must be in format [NNN]. Instead got " + indexWithParenthesis); | ||
} | ||
String indexString = indexWithParenthesis.substring(1, indexWithParenthesis.length() - 1); | ||
int index; | ||
try { | ||
index = Integer.parseInt(indexString); | ||
} catch (NumberFormatException nfex) { | ||
throw new IllegalArgumentException("Non-int array index. Must be in format [NNN]. Instead got " + indexWithParenthesis); | ||
} | ||
|
||
if (index < 0) { | ||
throw new ArrayIndexOutOfBoundsException("Invalid negative array index: " + indexWithParenthesis); | ||
} | ||
|
||
return index; | ||
} | ||
|
||
private ArrayNode toArrayNode() { | ||
if (!(node instanceof ArrayNode)) { | ||
throw new IllegalArgumentException( | ||
"Expected ARRAY node. Instead got " + node.getNodeType() + " at '" + incomingPath + "'"); | ||
} | ||
|
||
return (ArrayNode) node; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.