diff --git a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java index ac77b21e77..ec446c405d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java @@ -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 diff --git a/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java b/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java index d6213c6233..f914120378 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java @@ -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 comparator, JsonNode o) diff --git a/src/main/java/com/fasterxml/jackson/databind/node/MissingNode.java b/src/main/java/com/fasterxml/jackson/databind/node/MissingNode.java index 629e6ce24c..21a76762c3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/MissingNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/MissingNode.java @@ -32,7 +32,21 @@ private MissingNode() { } public 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() { diff --git a/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java b/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java index a78ba7a78f..b903963d43 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java @@ -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. diff --git a/src/main/java/com/fasterxml/jackson/databind/node/ValueNode.java b/src/main/java/com/fasterxml/jackson/databind/node/ValueNode.java index 86eb129773..2a7b0c81cd 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/ValueNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/ValueNode.java @@ -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 diff --git a/src/test/java/com/fasterxml/jackson/databind/TestAddByPointer.java b/src/test/java/com/fasterxml/jackson/databind/TestAddByPointer.java new file mode 100644 index 0000000000..ccf96bb045 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/TestAddByPointer.java @@ -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) { + } + } + +} diff --git a/src/test/java/com/fasterxml/jackson/databind/TestRemoveByPointer.java b/src/test/java/com/fasterxml/jackson/databind/TestRemoveByPointer.java new file mode 100644 index 0000000000..25366e18be --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/TestRemoveByPointer.java @@ -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) { + } + } + +}