Skip to content

Pointer based add and remove methods for #392 #393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
56 changes: 55 additions & 1 deletion src/main/java/com/fasterxml/jackson/databind/JsonNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,61 @@ public final JsonNode at(String jsonPtrExpr) {
}

protected abstract JsonNode _at(JsonPointer ptr);


private JsonNode _fromNullable(JsonNode n) {
return n != null ? n : MissingNode.getInstance();
}

/**
* Method for adding or updating the value at a given JSON pointer.
*
* @return This node to allow chaining or {@code value} if {@code ptr} is "/".
* @throws IllegalArgumentException if the path is invalid (e.g. empty, contains a name instead of an index)
* @throws UnsupportedOperationException if the targeted node does not support updates
* @since 2.6
*/
public final JsonNode add(JsonPointer ptr, JsonNode value) {
// In recursion we only match parent nodes, so this was an attempt to add to an empty pointer
if (ptr.matches()) {
throw new IllegalArgumentException("Cannot add to an empty path");
}

// FIXME Should mayMatchProperty check for empty strings?
if (ptr.getMatchingProperty().length() == 0 && !ptr.mayMatchElement()) {
// No possible match, return value as the new root element
return value;
} else if (ptr.tail().matches()) {
// Matched the parent node (hopefully a container)
return _add(ptr, value);
} else {
// Need to consume more of the pointer
_fromNullable(_at(ptr)).add(ptr.tail(), value);
return this;
}
}

protected abstract JsonNode _add(JsonPointer ptr, JsonNode value);

/**
* Method for removing the value at a given JSON pointer.
*
* @return Node removed, if any; null if none
* @throws IllegalArgumentException if the path is invalid
* @throws UnsupportedOperationException if the targeted node does not support removal
* @since 2.6
*/
public final JsonNode remove(JsonPointer ptr) {
if (ptr.matches()) {
return this;
} else if (ptr.tail().matches()) {
return _remove(ptr);
} else {
return _fromNullable(_at(ptr)).remove(ptr.tail());
}
}

protected abstract JsonNode _remove(JsonPointer ptr);

/*
/**********************************************************
/* Public API, type introspection
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@ public JsonNode path(int index) {
}
return MissingNode.getInstance();
}

@Override
protected JsonNode _add(JsonPointer ptr, JsonNode value) {
if (ptr.mayMatchElement()) {
return insert(ptr.getMatchingIndex(), value);
} else if (ptr.getMatchingProperty().equals("-")) {
return add(value);
} else {
throw new IllegalArgumentException("invalid array element: " + ptr);
}
}

@Override
protected JsonNode _remove(JsonPointer ptr) {
if (ptr.mayMatchElement()) {
return remove(ptr.getMatchingIndex());
} else {
throw new IllegalArgumentException("invalid array element: " + ptr);
}
}

@Override
public boolean equals(Comparator<JsonNode> comparator, JsonNode o)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,21 @@ private MissingNode() { }
public <T extends JsonNode> T deepCopy() { return (T) this; }

public static MissingNode getInstance() { return instance; }


@Override
protected JsonNode _add(JsonPointer ptr, JsonNode value)
{
// This is a path problem, not an unsupported operation
throw new IllegalArgumentException(ptr.toString());
}

@Override
protected JsonNode _remove(JsonPointer ptr)
{
// This is a path problem, not an unsupported operation
throw new IllegalArgumentException(ptr.toString());
}

@Override
public JsonNodeType getNodeType()
{
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ public JsonNode path(String fieldName)
return MissingNode.getInstance();
}

@Override
protected JsonNode _add(JsonPointer ptr, JsonNode value) {
// FIXME Should we be able to use mayMatchProperty?
if (ptr.getMatchingProperty().length() > 0) {
return set(ptr.getMatchingProperty(), value);
} else {
throw new IllegalArgumentException("invalid object property: " + ptr);
}
}

@Override
protected JsonNode _remove(JsonPointer ptr) {
// FIXME Should we be able to use mayMatchProperty?
if (ptr.getMatchingProperty().length() > 0) {
return remove(ptr.getMatchingProperty());
} else {
throw new IllegalArgumentException("invalid object property: " + ptr);
}
}

/**
* Method to use for accessing all fields (with both names
* and values) of this JSON Object.
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/node/ValueNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ public void serializeWithType(JsonGenerator jg, SerializerProvider provider,
@Override
public final boolean hasNonNull(String fieldName) { return false; }

@Override
protected JsonNode _add(JsonPointer ptr, JsonNode value) {
throw new UnsupportedOperationException("value");
}

@Override
protected JsonNode _remove(JsonPointer ptr) {
throw new UnsupportedOperationException("value");
}

/*
**********************************************************************
* Find methods: all "leaf" nodes return the same for these
Expand Down
127 changes: 127 additions & 0 deletions src/test/java/com/fasterxml/jackson/databind/TestAddByPointer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.fasterxml.jackson.databind;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* Basic tests to ensure we can add into the tree using JSON pointers.
*/
public class TestAddByPointer extends BaseMapTest {

/*
/**********************************************************
/* JsonNode
/**********************************************************
*/

// TODO It would be nice to have a "TestNode" to isolate implementations in the base class

public void testAddEmpty() {
try {
ArrayNode n = objectMapper().createArrayNode();
n.add(JsonPointer.compile(""), n.numberNode(1));
fail();
} catch (IllegalArgumentException expected) {
}
}

public void testAddRootPath() {
ArrayNode n = objectMapper().createArrayNode();
JsonNode v = n.numberNode(1);
assertEquals(v, n.add(JsonPointer.compile("/"), v));
}

public void testAddMissing() {
try {
ObjectNode n = objectMapper().createObjectNode();
n.add(JsonPointer.compile("/o/i"), n.numberNode(1));
fail();
} catch (IllegalArgumentException expected) {
}
}

/*
/**********************************************************
/* ArrayNode
/**********************************************************
*/

public void testAddArrayDepth1() {
ArrayNode n = objectMapper().createArrayNode();

n.add(JsonPointer.compile("/0"), n.numberNode(1));
assertEquals(1, n.get(0).asInt());

n.add(JsonPointer.compile("/0"), n.numberNode(2));
assertEquals(2, n.get(0).asInt());
assertEquals(1, n.get(1).asInt());

n.add(JsonPointer.compile("/-"), n.numberNode(3)); // special case: append
assertEquals(2, n.get(0).asInt());
assertEquals(1, n.get(1).asInt());
assertEquals(3, n.get(2).asInt());
}

public void testAddArrayDepth2() {
ObjectNode n = objectMapper().createObjectNode();
n.set("a", n.arrayNode());
JsonPointer A_APPEND = JsonPointer.compile("/a/-");
n.add(A_APPEND, n.numberNode(1));
n.add(A_APPEND, n.numberNode(2));
n.add(A_APPEND, n.numberNode(3));
assertEquals(1, n.at("/a/0").asInt());
assertEquals(2, n.at("/a/1").asInt());
assertEquals(3, n.at("/a/2").asInt());
}

/**
* RFC 6902 isn't clear about what this behavior should be: error, silent
* ignore or perform insert. We error out to allow higher level
* implementations the opportunity to handle the problem as they see fit.
*/
public void testAddInvalidArrayElementPointer() {
try {
ArrayNode n = objectMapper().createArrayNode();
n.add(JsonPointer.compile("/a"), n.numberNode(1));
fail();
} catch (IllegalArgumentException expected) {
}
}

/*
/**********************************************************
/* ObjectNode
/**********************************************************
*/

public void testAddObjectDepth1() {
ObjectNode n = objectMapper().createObjectNode();
n.add(JsonPointer.compile("/a"), n.numberNode(1));
assertEquals(1, n.get("a").asInt());
}

public void testAddObjectDepth2() {
ObjectNode n = objectMapper().createObjectNode();
n.set("o", n.objectNode());
n.add(JsonPointer.compile("/o/i"), n.numberNode(1));
assertEquals(1, n.at("/o/i").asInt());
}

/*
/**********************************************************
/* ValueNode
/**********************************************************
*/

public void testAddValue() {
try {
NumericNode n = objectMapper().getNodeFactory().numberNode(1);
n.add(JsonPointer.compile("/0"), objectMapper().getNodeFactory().numberNode(2));
fail();
} catch (UnsupportedOperationException expected) {
}
}

}
108 changes: 108 additions & 0 deletions src/test/java/com/fasterxml/jackson/databind/TestRemoveByPointer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.fasterxml.jackson.databind;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* Basic tests to ensure we can remove from the tree using JSON pointers.
*/
public class TestRemoveByPointer extends BaseMapTest {

/*
/**********************************************************
/* JsonNode
/**********************************************************
*/

// TODO It would be nice to have a "TestNode" to isolate implementations in the base class

public void testRemoveEmpty() {
JsonNode n = objectMapper().createArrayNode();
assertEquals(n, n.remove(JsonPointer.compile("")));
}

public void testRemoveMissing() {
try {
JsonNode n = objectMapper().createObjectNode();
n.remove(JsonPointer.compile("/o/i"));
fail();
} catch (IllegalArgumentException expected) {
}
}

/*
/**********************************************************
/* ArrayNode
/**********************************************************
*/

public void testRemoveArrayRootPath() {
try {
JsonNode n = objectMapper().createArrayNode();
assertEquals(n, n.remove(JsonPointer.compile("/")));
fail();
} catch (IllegalArgumentException expected) {
}
}

public void testRemoveArrayDepth1() {
JsonNode n = objectMapper().createArrayNode().add(1).add(2).add(3);

n.remove(JsonPointer.compile("/1"));
assertEquals(1, n.get(0).asInt());
assertEquals(3, n.get(1).asInt());
}

public void testRemoveArrayDepth2() {
ObjectNode n = objectMapper().createObjectNode();
n.set("a", n.arrayNode().add(1).add(2).add(3));

n.remove(JsonPointer.compile("/a/1"));
assertEquals(1, n.at("/a/0").asInt());
assertEquals(3, n.at("/a/1").asInt());
}

/*
/**********************************************************
/* ObjectNode
/**********************************************************
*/

public void testObjectRemoveRootPath() {
try {
JsonNode n = objectMapper().createObjectNode();
assertEquals(n, n.remove(JsonPointer.compile("/")));
fail();
} catch (IllegalArgumentException expected) {
}
}

public void testRemoveObjectDepth1() {
ObjectNode n = objectMapper().createObjectNode();
n.set("i", n.numberNode(1));
assertEquals(1, n.remove(JsonPointer.compile("/i")).asInt());
}

public void testRemoveObjectDepth2() {
ObjectNode n = objectMapper().createObjectNode();
n.set("o", n.objectNode().set("i", n.numberNode(1)));
assertEquals(1, n.remove(JsonPointer.compile("/o/i")).asInt());
}

/*
/**********************************************************
/* ValueNode
/**********************************************************
*/

public void testValueRemoveRootPath() {
try {
NumericNode n = objectMapper().getNodeFactory().numberNode(1);
assertEquals(n, n.remove(JsonPointer.compile("/")));
fail();
} catch (UnsupportedOperationException expected) {
}
}

}