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
51 changes: 50 additions & 1 deletion src/main/java/com/fasterxml/jackson/databind/JsonNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,56 @@ public final JsonNode at(String jsonPtrExpr) {
}

protected abstract JsonNode _at(JsonPointer ptr);


/**
* 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.4
*/
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
return _at(ptr).add(ptr.tail(), value);
}
}

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.4
*/
public final JsonNode remove(JsonPointer ptr) {
if (ptr.matches()) {
return this;
} else if (ptr.tail().matches()) {
return _remove(ptr);
} else {
return _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 @@ -87,6 +87,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);
}
}

/*
/**********************************************************
/* Public API, serialization
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 @@ -106,6 +106,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
12 changes: 12 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 @@ -107,6 +107,18 @@ public final boolean hasNonNull(String fieldName)
{
return false;
}

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

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

/*
**********************************************************************
Expand Down
119 changes: 119 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,119 @@
package com.fasterxml.jackson.databind;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
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 {

private final JsonNode ONE = new IntNode(1);

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

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

public void testAddEmpty() {
try {
JsonNode n = new ArrayNode(JsonNodeFactory.instance);
n.add(JsonPointer.compile(""), ONE);
fail();
} catch (IllegalArgumentException expected) {
}
}

public void testAddRootPath() {
JsonNode n = new ArrayNode(JsonNodeFactory.instance);
assertEquals(ONE, n.add(JsonPointer.compile("/"), ONE));
}

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

public void testAddArrayDepth1() {
JsonNode n = new ArrayNode(JsonNodeFactory.instance);

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

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

n.add(JsonPointer.compile("/-"), new IntNode(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() {
JsonNode n = new ObjectNode(JsonNodeFactory.instance);
((ObjectNode) n).set("a", new ArrayNode(JsonNodeFactory.instance));
JsonPointer A_APPEND = JsonPointer.compile("/a/-");
n.add(A_APPEND, new IntNode(1));
n.add(A_APPEND, new IntNode(2));
n.add(A_APPEND, new IntNode(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 {
JsonNode n = new ArrayNode(JsonNodeFactory.instance);
n.add(JsonPointer.compile("/a"), ONE);
fail();
} catch (IllegalArgumentException expected) {
}
}

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

public void testAddObjectDepth1() {
JsonNode n = new ObjectNode(JsonNodeFactory.instance);
n.add(JsonPointer.compile("/a"), ONE);
assertEquals(1, n.get("a").asInt());
}

public void testAddObjectDepth2() {
JsonNode n = new ObjectNode(JsonNodeFactory.instance);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion here; take JsonNodeFactory.instance, use its factory methods for creating ObjectNodes (etc) -- same for other usage too of course. Or, if tests have ObjectMapper, use array/object node factory methods.

((ObjectNode) n).set("o", new ObjectNode(JsonNodeFactory.instance));
n.add(JsonPointer.compile("/o/i"), ONE);
assertEquals(1, n.at("/o/i").asInt());
}

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

public void testAddValue() {
try {
ONE.add(JsonPointer.compile("/0"), ONE);
fail();
} catch (UnsupportedOperationException expected) {
}
}

}
100 changes: 100 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,100 @@
package com.fasterxml.jackson.databind;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
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 = new ArrayNode(JsonNodeFactory.instance);
assertEquals(n, n.remove(JsonPointer.compile("")));
}

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

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

public void testRemoveArrayDepth1() {
JsonNode n = new ArrayNode(JsonNodeFactory.instance).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() {
JsonNode n = new ObjectNode(JsonNodeFactory.instance);
((ObjectNode) n).set("a", new ArrayNode(JsonNodeFactory.instance).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 = new ObjectNode(JsonNodeFactory.instance);
assertEquals(n, n.remove(JsonPointer.compile("/")));
fail();
} catch (IllegalArgumentException expected) {
}
}

public void testRemoveObjectDepth1() {
JsonNode n = new ObjectNode(JsonNodeFactory.instance).set("i", new IntNode(1));
assertEquals(1, n.remove(JsonPointer.compile("/i")).asInt());
}

public void testRemoveObjectDepth2() {
JsonNode n = new ObjectNode(JsonNodeFactory.instance);
((ObjectNode) n).set("o", new ObjectNode(JsonNodeFactory.instance).set("i", new IntNode(1)));
assertEquals(1, n.remove(JsonPointer.compile("/o/i")).asInt());
}

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

public void testValueRemoveRootPath() {
try {
JsonNode n = new IntNode(1);
assertEquals(n, n.remove(JsonPointer.compile("/")));
fail();
} catch (UnsupportedOperationException expected) {
}
}

}