Skip to content

Commit

Permalink
Config: addressing array elements via property paths #216
Browse files Browse the repository at this point in the history
* subclass-based impl
  • Loading branch information
andrus committed Apr 7, 2018
1 parent 0d3de90 commit cd5a7f4
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 227 deletions.

This file was deleted.

This file was deleted.

@@ -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;
}

}
@@ -1,43 +1,41 @@
package io.bootique.config.jackson; package io.bootique.config.jackson;


import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;


import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;


/** /**
* Overrides JsonNode object values from a map of properties. * Overrides JsonNode object values from a map of properties.
* *
* @since 0.17 * @since 0.17
*/ */
public class InPlaceMapOverrider implements Function<JsonNode, JsonNode> { public class InPlaceMapOverrider implements Function<JsonNode, JsonNode> {


private Map<String, String> properties; private Map<String, String> properties;


public InPlaceMapOverrider(Map<String, String> properties) { public InPlaceMapOverrider(Map<String, String> properties) {
this.properties = properties; this.properties = properties;
} }


@Override @Override
public JsonNode apply(JsonNode t) { public JsonNode apply(JsonNode t) {
properties.entrySet().forEach(e -> { properties.entrySet().forEach(e -> {


PathSegment target = lastPathComponent(t, e.getKey()); PathSegment target = lastPathComponent(t, e.getKey());
target.fillMissingParents(); target.fillMissingParents();


if (!(target.getParentNode() instanceof ObjectNode)) { if (target.getParent() == null) {
throw new IllegalArgumentException("Invalid property '" + e.getKey() + "'"); throw new IllegalArgumentException("No parent node");
} }


ObjectNode parentObjectNode = (ObjectNode) target.getParentNode(); target.getParent().writeChild(target.getIncomingPath(), e.getValue());
parentObjectNode.put(target.getIncomingPath(), e.getValue()); });
});


return t; return t;
} }


protected PathSegment lastPathComponent(JsonNode t, String path) { protected PathSegment lastPathComponent(JsonNode t, String path) {
return new PathSegment(t, path).lastPathComponent().get(); return PathSegment.create(t, path).lastPathComponent().get();
} }
} }
126 changes: 126 additions & 0 deletions bootique/src/main/java/io/bootique/config/jackson/IndexSegment.java
@@ -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;
}
}
Expand Up @@ -73,7 +73,7 @@ protected JsonNode findChild(String path) {


// or we just make it case-sensitive like the rest of the config... // or we just make it case-sensitive like the rest of the config...


return new CiPathSegment(rootNode, path).lastPathComponent().map(t -> t.getNode()) return new CiPropertySegment(rootNode, path).lastPathComponent().map(t -> t.getNode())
.orElse(new ObjectNode(null)); .orElse(new ObjectNode(null));
} }


Expand Down

0 comments on commit cd5a7f4

Please sign in to comment.