Skip to content
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) {
}
}

}