From 56fb5d3fd3c69157d6625e94a43fd60aa57bbf72 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 5 Jul 2022 19:34:08 -0700 Subject: [PATCH] Continue with #1980 --- .../fasterxml/jackson/databind/JsonNode.java | 67 ++++++++++++++++++- .../jackson/databind/node/ArrayNode.java | 8 +-- .../jackson/databind/node/BaseJsonNode.java | 26 ++++--- .../jackson/databind/node/ObjectNode.java | 8 +-- .../jackson/databind/node/WithPathTest.java | 34 ++++++++++ 5 files changed, 123 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java index a0da32dcf8..f7d460ef36 100644 --- a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -40,6 +41,45 @@ public abstract class JsonNode extends JsonSerializable.Base // i.e. implements JsonSerializable implements TreeNode, Iterable { + /** + * Configuration setting used with {@link JsonNode#withObject(JsonPointer)} + * method overrides, to indicate which overwrites are acceptable if the + * path pointer indicates has incompatible nodes (for example, instead + * of Object node a Null node is encountered). + * Overwrite means that the existing value is replaced with compatible type, + * potentially losing existing values or even sub-trees. + *

+ * Default value if {@code NULLS} which only allows Null-value nodes + * to be replaced but no other types. + * + * @since 2.14 + */ + public enum OverwriteMode { + /** + * Mode in which no values may be overwritten, not even {@code NullNode}s; + * only compatible paths may be traversed. + */ + NONE, + + /** + * Mode in which explicit {@code NullNode}s may be replaced but no other + * node types. + */ + NULLS, + + /** + * Mode in which all scalar value nodes may be replaced, but not + * Array or Object nodes. + */ + SCALARS, + + /** + * Mode in which all incompatible node types may be replaced, including + * Array and Object nodes where necessary. + */ + ALL; + } + /* /********************************************************** /* Construction, related @@ -1090,6 +1130,24 @@ public T withObject(String propertyName) { +getClass().getName()+"), cannot call withObject() on it"); } + /** + * Same as {@link #withObject(JsonPointer, OverwriteMode, boolean)} but + * with defaults of {@code OvewriteMode#NULLS} (overwrite mode) + * and {@code true} for {@code preferIndex} (that is, will try to + * consider {@link JsonPointer} segments index if at all possible + * and only secondarily as property name + * + * @param ptr Pointer that indicates path to use for Object value to return + * (potentially creating as necessary) + * + * @return ObjectNode found or created + * + * @since 2.14 + */ + public final ObjectNode withObject(JsonPointer ptr) { + return withObject(ptr, OverwriteMode.NULLS, true); + } + /** * Method that can be called on Object nodes, to access a Object-valued * node pointed to by given {@link JsonPointer}, if such a node exists: @@ -1098,9 +1156,16 @@ public T withObject(String propertyName) { * or if property exists and has value that is not Object node, * {@link UnsupportedOperationException} is thrown * + * @param ptr Pointer that indicates path to use for Object value to return + * (potentially creating as necessary) + * @param overwriteMode Defines w + * + * @return ObjectNode found or created + * * @since 2.14 */ - public T withObject(JsonPointer ptr) { + public ObjectNode withObject(JsonPointer ptr, + OverwriteMode overwriteMode, boolean preferIndex) { // To avoid abstract method, base implementation just fails throw new UnsupportedOperationException("`withObject(JsonPointer)` not implemented by " +getClass().getName()); 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 5e5adde904..ac01855747 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java @@ -68,10 +68,10 @@ public ArrayNode deepCopy() return ret; } - @SuppressWarnings("unchecked") @Override - protected T _withObjectCreatePath(JsonPointer origPtr, - JsonPointer currentPtr) + protected ObjectNode _withObjectCreatePath(JsonPointer origPtr, + JsonPointer currentPtr, + OverwriteMode overwriteMode, boolean preferIndex) { // With Arrays, bit different; the first entry needs to be index if (!currentPtr.mayMatchElement()) { @@ -99,7 +99,7 @@ protected T _withObjectCreatePath(JsonPointer origPtr, currentNode = currentNode.putObject(currentPtr.getMatchingProperty()); currentPtr = currentPtr.tail(); } - return (T) currentNode; + return (ObjectNode) currentNode; } /* diff --git a/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java b/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java index 93fe59a472..13612c1299 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java @@ -110,22 +110,23 @@ public JsonParser.NumberType numberType() { */ @Override - public T withObject(JsonPointer ptr) { + public ObjectNode withObject(JsonPointer ptr, + OverwriteMode overwriteMode, boolean preferIndex) { if (!isObject()) { // To avoid abstract method, base implementation just fails _reportWrongNodeType("Can only call `withObject()` on `ObjectNode`, not `%s`", getClass().getName()); } - return _withObject(ptr, ptr); + return _withObject(ptr, ptr, overwriteMode, preferIndex); } - @SuppressWarnings("unchecked") - protected T _withObject(JsonPointer origPtr, - JsonPointer currentPtr) + protected ObjectNode _withObject(JsonPointer origPtr, + JsonPointer currentPtr, + OverwriteMode overwriteMode, boolean preferIndex) { if (currentPtr.matches()) { - if (this.isObject()) { - return (T) this; + if (this instanceof ObjectNode) { + return (ObjectNode) this; } return _reportWrongNodeType( "`JsonNode` matching `JsonPointer` \"%s\" must be `ObjectNode`, not `%s`", @@ -133,10 +134,12 @@ protected T _withObject(JsonPointer origPtr, getClass().getName()); } JsonNode n = _at(currentPtr); + // If there's a path, follow it if ((n != null) && (n instanceof BaseJsonNode)) { - return ((BaseJsonNode) n)._withObject(origPtr, currentPtr.tail()); + return ((BaseJsonNode) n)._withObject(origPtr, currentPtr.tail(), + overwriteMode, preferIndex); } - return _withObjectCreatePath(origPtr, currentPtr); + return _withObjectCreatePath(origPtr, currentPtr, overwriteMode, preferIndex); } /** @@ -144,8 +147,9 @@ protected T _withObject(JsonPointer origPtr, * or throwing an exception if not. If construction successful, needs to return * the innermost {@code ObjectNode} constructed. */ - protected T _withObjectCreatePath(JsonPointer origPtr, - JsonPointer currentPtr) + protected ObjectNode _withObjectCreatePath(JsonPointer origPtr, + JsonPointer currentPtr, + OverwriteMode overwriteMode, boolean preferIndex) { // Cannot traverse non-container nodes: return _reportWrongNodeType( 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 e2d69ee41b..4f93768d75 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java @@ -60,10 +60,10 @@ public ObjectNode deepCopy() return ret; } - @SuppressWarnings("unchecked") @Override - protected T _withObjectCreatePath(JsonPointer origPtr, - JsonPointer currentPtr) + protected ObjectNode _withObjectCreatePath(JsonPointer origPtr, + JsonPointer currentPtr, + OverwriteMode overwriteMode, boolean preferIndex) { ObjectNode currentNode = this; while (!currentPtr.matches()) { @@ -71,7 +71,7 @@ protected T _withObjectCreatePath(JsonPointer origPtr, currentNode = currentNode.putObject(currentPtr.getMatchingProperty()); currentPtr = currentPtr.tail(); } - return (T) currentNode; + return currentNode; } /* diff --git a/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java b/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java index 2f78e306e6..cd270f90c0 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java @@ -40,6 +40,38 @@ public void testValidWithObjectSimple() throws Exception root.toString()); } + public void testObjectPathWithReplace() throws Exception + { + final JsonPointer abPath = JsonPointer.compile("/a/b"); + ObjectNode root = MAPPER.createObjectNode(); + root.put("a", 13); + + // First, without replacement (default) get exception + try { + root.withObject(abPath); + fail("Should not pass"); + } catch (UnsupportedOperationException e) { + verifyException(e, "cannot traverse non-container"); + } + + // Except fine via nulls (by default) + /* + root.putNull("a"); + root.withObject(abPath).put("value", 42); + assertEquals(a2q("{'a':{'b':{'value':42}}}"), + root.toString()); + + // but not if prevented + root = (ObjectNode) MAPPER.readTree(a2q("{'a':null}")); + try { + root.withObject(abPath); + fail("Should not pass"); + } catch (UnsupportedOperationException e) { + verifyException(e, "cannot traverse non-container"); + } + */ + } + public void testValidWithObjectWithArray() throws Exception { ObjectNode root = MAPPER.createObjectNode(); @@ -56,5 +88,7 @@ public void testValidWithObjectWithArray() throws Exception match.put("value2", true); assertEquals(a2q("{'arr':[null,null,{'value':42,'value2':true}]}"), root.toString()); + + // And even more! `null`s can be replaced } }