diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 2933ba28f1..50f80e5ec7 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -55,6 +55,8 @@ Project: jackson-databind custom `NullValueProvider` #3535: Replace `JsonNode.with()` with `JsonNode.withObject()` #3559: Support `null`-valued `Map` fields with "any setter" +#3568: Change `JsonNode.with(String)` and `withArray(String)` to consider + argument as `JsonPointer` if valid expression 2.13.4 (not yet released) diff --git a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java index 8dfa785465..9cf727ea75 100644 --- a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java @@ -1119,22 +1119,33 @@ public final List findParents(String fieldName) */ /** - * Method that can be called on Object nodes, to access a property - * that has Object value; or if no such property exists, to create, - * add and return such Object node. - * If the node method is called on is not Object node, - * or if property exists and has value that is not Object node, - * {@link UnsupportedOperationException} is thrown + * Short-cut equivalent to: + *
+     *   withObject(JsonPointer.compile(expr);
+     *
+ * see {@link #withObject(JsonPointer)} for full explanation. * - * @param propertyName Name of property for the {@link ObjectNode} + * @param expr {@link JsonPointer} expression to use * * @return {@link ObjectNode} found or created * * @since 2.14 */ - public ObjectNode withObject(String propertyName) { - throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but " - +getClass().getName()+"), cannot call `withObject()` on it"); + public final ObjectNode withObject(String expr) { + return withObject(JsonPointer.compile(expr)); + } + + /** + * Short-cut equivalent to: + *
+     *  withObject(JsonPointer.compile(expr), overwriteMode, preferIndex);
+     *
+ * + * @since 2.14 + */ + public final ObjectNode withObject(String expr, + OverwriteMode overwriteMode, boolean preferIndex) { + return withObject(JsonPointer.compile(expr), overwriteMode, preferIndex); } /** @@ -1231,31 +1242,68 @@ public ObjectNode withObject(JsonPointer ptr, } /** + * Method that works in one of possible ways, depending on whether + * {@code exprOrProperty} is a valid {@link JsonPointer} expression or + * not (valid expression is either empty String {@code ""} or starts + * with leading slash {@code /} character). + * If it is, works as a short-cut to: + *
+     *  withObject(JsonPointer.compile(exprOrProperty));
+     *
+ * If it is NOT a valid {@link JsonPointer} expression, value is taken + * as a literal Object property name and traversed like a single-segment + * {@link JsonPointer}. + *

+ * NOTE: before Jackson 2.14 behavior was always that of non-expression usage; + * that is, {@code exprOrProperty} was always considered as a simple property name. + * * @deprecated Since 2.14 use {@code withObject(String)} instead */ - @SuppressWarnings("unchecked") @Deprecated // since 2.14 - public final T with(String propertyName) { - return (T) withObject(propertyName); + public T with(String exprOrProperty) { + throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but " + +getClass().getName()+"), cannot call `with(String)` on it"); } /** - * Method that can be called on {@link ObjectNode} nodes, to access a property - * that has Array value; or if no such property exists, to create, - * add and return such Array node. - * If the node method is called on is not Object node, - * or if property exists and has value that is not Array node, - * {@link UnsupportedOperationException} is thrown + * Method that works in one of possible ways, depending on whether + * {@code exprOrProperty} is a valid {@link JsonPointer} expression or + * not (valid expression is either empty String {@code ""} or starts + * with leading slash {@code /} character). + * If it is, works as a short-cut to: + *

+     *  withObject(JsonPointer.compile(exprOrProperty));
+     *
+ * If it is NOT a valid {@link JsonPointer} expression, value is taken + * as a literal Object property name and traversed like a single-segment + * {@link JsonPointer}. + *

+ * NOTE: before Jackson 2.14 behavior was always that of non-expression usage; + * that is, {@code exprOrProperty} was always considered as a simple property name. * - * @param propertyName Name of property for the {@link ArrayNode} + * @param exprOrProperty Either {@link JsonPointer} expression for full access (if valid + * pointer expression), or the name of property for the {@link ArrayNode}. * * @return {@link ArrayNode} found or created */ - public T withArray(String propertyName) { + public T withArray(String exprOrProperty) { throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but `" +getClass().getName()+")`, cannot call `withArray()` on it"); } + /** + * Short-cut equivalent to: + *

+     *  withArray(JsonPointer.compile(expr), overwriteMode, preferIndex);
+     *
+ * + * @since 2.14 + */ + public ArrayNode withArray(String expr, + OverwriteMode overwriteMode, boolean preferIndex) { + return withArray(JsonPointer.compile(expr), overwriteMode, preferIndex); + } + /** * Same as {@link #withArray(JsonPointer, OverwriteMode, boolean)} but * with defaults of {@code OvewriteMode#NULLS} (overwrite mode) 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 f0bdd8721e..507a65483d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java @@ -71,6 +71,28 @@ public ArrayNode deepCopy() /********************************************************** */ + @SuppressWarnings("unchecked") + @Deprecated + @Override + public ObjectNode with(String exprOrProperty) { + JsonPointer ptr = _jsonPointerIfValid(exprOrProperty); + if (ptr != null) { + return withObject(ptr); + } + return super.with(exprOrProperty); // to give failure + } + + @SuppressWarnings("unchecked") + @Override + public ArrayNode withArray(String exprOrProperty) + { + JsonPointer ptr = _jsonPointerIfValid(exprOrProperty); + if (ptr != null) { + return withArray(ptr); + } + return super.withArray(exprOrProperty); // to give failure + } + @Override protected ObjectNode _withObject(JsonPointer origPtr, JsonPointer currentPtr, 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 aafca14fca..6a45b9d155 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java @@ -260,4 +260,13 @@ protected T _reportWrongNodeType(String msgTemplate, Object...args) { protected T _reportWrongNodeOperation(String msgTemplate, Object...args) { throw new UnsupportedOperationException(String.format(msgTemplate, args)); } + + // @since 2.14 + protected JsonPointer _jsonPointerIfValid(String exprOrProperty) { + if (exprOrProperty.isEmpty() || exprOrProperty.charAt(0) == '/') { + return JsonPointer.compile(exprOrProperty); + } + return null; + } + } 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 37cc01467c..f3c6b19e53 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java @@ -67,6 +67,50 @@ public ObjectNode deepCopy() /********************************************************** */ + @SuppressWarnings("unchecked") + @Deprecated + @Override + public ObjectNode with(String exprOrProperty) { + JsonPointer ptr = _jsonPointerIfValid(exprOrProperty); + if (ptr != null) { + return withObject(ptr); + } + JsonNode n = _children.get(exprOrProperty); + if (n != null) { + if (n instanceof ObjectNode) { + return (ObjectNode) n; + } + throw new UnsupportedOperationException("Property '" + exprOrProperty + + "' has value that is not of type `ObjectNode` (but `" + n + .getClass().getName() + "`)"); + } + ObjectNode result = objectNode(); + _children.put(exprOrProperty, result); + return result; + } + + @SuppressWarnings("unchecked") + @Override + public ArrayNode withArray(String exprOrProperty) + { + JsonPointer ptr = _jsonPointerIfValid(exprOrProperty); + if (ptr != null) { + return withArray(ptr); + } + JsonNode n = _children.get(exprOrProperty); + if (n != null) { + if (n instanceof ArrayNode) { + return (ArrayNode) n; + } + throw new UnsupportedOperationException("Property '" + exprOrProperty + + "' has value that is not of type `ArrayNode` (but `" + n + .getClass().getName() + "`)"); + } + ArrayNode result = arrayNode(); + _children.put(exprOrProperty, result); + return result; + } + @Override protected ObjectNode _withObject(JsonPointer origPtr, JsonPointer currentPtr, @@ -239,40 +283,6 @@ public Iterator> fields() { return _children.entrySet().iterator(); } - @Override - public ObjectNode withObject(String propertyName) { - JsonNode n = _children.get(propertyName); - if (n != null) { - if (n instanceof ObjectNode) { - return (ObjectNode) n; - } - throw new UnsupportedOperationException("Property '" + propertyName - + "' has value that is not of type `ObjectNode` (but `" + n - .getClass().getName() + "`)"); - } - ObjectNode result = objectNode(); - _children.put(propertyName, result); - return result; - } - - @SuppressWarnings("unchecked") - @Override - public ArrayNode withArray(String propertyName) - { - JsonNode n = _children.get(propertyName); - if (n != null) { - if (n instanceof ArrayNode) { - return (ArrayNode) n; - } - throw new UnsupportedOperationException("Property '" + propertyName - + "' has value that is not of type `ArrayNode` (but `" + n - .getClass().getName() + "`)"); - } - ArrayNode result = arrayNode(); - _children.put(propertyName, result); - return result; - } - @Override public boolean equals(Comparator comparator, JsonNode o) { diff --git a/src/test/java/com/fasterxml/jackson/databind/node/JsonPointerWithNodeTest.java b/src/test/java/com/fasterxml/jackson/databind/node/JsonPointerAtNodeTest.java similarity index 98% rename from src/test/java/com/fasterxml/jackson/databind/node/JsonPointerWithNodeTest.java rename to src/test/java/com/fasterxml/jackson/databind/node/JsonPointerAtNodeTest.java index aceaca0e9d..fc599dd82a 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/JsonPointerWithNodeTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/JsonPointerAtNodeTest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.databind.*; -public class JsonPointerWithNodeTest +public class JsonPointerAtNodeTest extends BaseMapTest { private final ObjectMapper MAPPER = newJsonMapper(); diff --git a/src/test/java/com/fasterxml/jackson/databind/node/NodeJDKSerializationTest.java b/src/test/java/com/fasterxml/jackson/databind/node/NodeJDKSerializationTest.java index 7f15fece04..d0c62d4b82 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/NodeJDKSerializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/NodeJDKSerializationTest.java @@ -25,7 +25,7 @@ public void testObjectNodeSerialization() throws Exception root.put("answer", 42); ArrayNode arr = root.withArray("matrix"); arr.add(1).add(12345678901L).add(true).add("..."); - ObjectNode misc = root.withObject("misc"); + ObjectNode misc = root.withObject("/misc"); misc.put("value", 0.25); testNodeRoundtrip(root); diff --git a/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java b/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java index 6c5fdb18af..7314526365 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java @@ -303,7 +303,7 @@ public void testValidWith() throws Exception { ObjectNode root = MAPPER.createObjectNode(); assertEquals("{}", MAPPER.writeValueAsString(root)); - JsonNode child = root.withObject("prop"); + JsonNode child = root.withObject("/prop"); assertTrue(child instanceof ObjectNode); assertEquals("{\"prop\":{}}", MAPPER.writeValueAsString(root)); } @@ -321,7 +321,7 @@ public void testInvalidWith() throws Exception { JsonNode root = MAPPER.createArrayNode(); try { // should not work for non-ObjectNode nodes: - root.withObject("prop"); + root.with("prop"); fail("Expected exception"); } catch (UnsupportedOperationException e) { verifyException(e, "not of type `ObjectNode`"); @@ -330,7 +330,7 @@ public void testInvalidWith() throws Exception ObjectNode root2 = MAPPER.createObjectNode(); root2.put("prop", 13); try { // should not work for non-ObjectNode nodes: - root2.withObject("prop"); + root2.with("prop"); fail("Expected exception"); } catch (UnsupportedOperationException e) { verifyException(e, "has value that is not"); 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 f6ceb5a060..6cf2a128a2 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java @@ -23,27 +23,44 @@ public void testValidWithObjectTrivial() throws Exception } public void testValidWithObjectSimpleExisting() throws Exception + { + _testValidWithObjectSimpleExisting(true); + _testValidWithObjectSimpleExisting(false); + } + + private void _testValidWithObjectSimpleExisting(boolean compile) throws Exception { final String DOC_STR = a2q("{'a':{'b':42,'c':{'x':1}}}"); JsonNode doc = MAPPER.readTree(DOC_STR); - ObjectNode match = doc.withObject(JsonPointer.compile("/a")); + ObjectNode match = compile + ? doc.withObject(JsonPointer.compile("/a")) + : doc.withObject("/a"); assertNotNull(match); assertTrue(match.isObject()); assertEquals(a2q("{'b':42,'c':{'x':1}}"), match.toString()); // should not modify the doc assertEquals(DOC_STR, doc.toString()); - match = doc.withObject(JsonPointer.compile("/a/c")); + match = compile + ? doc.withObject(JsonPointer.compile("/a/c")) + : doc.withObject("/a/c"); assertNotNull(match); assertEquals(a2q("{'x':1}"), match.toString()); // should not modify the doc assertEquals(DOC_STR, doc.toString()); } - public void testValidWithObjectSimpleCreate() throws Exception + public void testValidWithObjectSimpleCreate() throws Exception { + _testValidWithObjectSimpleCreate(true); + _testValidWithObjectSimpleCreate(false); + } + + private void _testValidWithObjectSimpleCreate(boolean compile) throws Exception { ObjectNode root = MAPPER.createObjectNode(); - ObjectNode match = root.withObject(JsonPointer.compile("/a/b")); + ObjectNode match = compile + ? root.withObject(JsonPointer.compile("/a/b")) + : root.withObject("/a/b"); assertTrue(match.isObject()); assertEquals(a2q("{}"), match.toString()); match.put("value", 42); @@ -52,7 +69,9 @@ public void testValidWithObjectSimpleCreate() throws Exception root.toString()); // and with that - ObjectNode match2 = root.withObject(JsonPointer.compile("/a/b")); + ObjectNode match2 = compile + ? root.withObject(JsonPointer.compile("/a/b")) + : root.withObject("/a/b"); assertSame(match, match2); match.put("value2", true); @@ -60,17 +79,29 @@ public void testValidWithObjectSimpleCreate() throws Exception root.toString()); } - public void testValidWithObjectSimpleModify() throws Exception + public void testValidWithObjectSimpleModify() throws Exception { + _testValidWithObjectSimpleModify(true); + _testValidWithObjectSimpleModify(false); + } + + private void _testValidWithObjectSimpleModify(boolean compile) throws Exception { final String DOC_STR = a2q("{'a':{'b':42}}"); JsonNode doc = MAPPER.readTree(DOC_STR); - ObjectNode match = doc.withObject(JsonPointer.compile("/a/d")); + ObjectNode match = compile + ? doc.withObject(JsonPointer.compile("/a/d")) + : doc.withObject("/a/d"); assertNotNull(match); assertEquals("{}", match.toString()); assertEquals(a2q("{'a':{'b':42,'d':{}}}"), doc.toString()); } - public void testObjectPathWithReplace() throws Exception + public void testObjectPathWithReplace() throws Exception { + _testObjectPathWithReplace(true); + _testObjectPathWithReplace(false); + } + + private void _testObjectPathWithReplace(boolean compile) throws Exception { final JsonPointer abPath = JsonPointer.compile("/a/b"); ObjectNode root = MAPPER.createObjectNode(); @@ -81,7 +112,11 @@ public void testObjectPathWithReplace() throws Exception // Except fine via nulls (by default) root.putNull("a"); - root.withObject(abPath).put("value", 42); + if (compile) { + root.withObject(abPath).put("value", 42); + } else { + root.withObject("/a/b").put("value", 42); + } assertEquals(a2q("{'a':{'b':{'value':42}}}"), root.toString()); @@ -90,25 +125,36 @@ public void testObjectPathWithReplace() throws Exception _verifyObjectReplaceFail(root, abPath, OverwriteMode.NONE); } - public void testValidWithObjectWithArray() throws Exception + public void testValidWithObjectWithArray() throws Exception { + _testValidWithObjectWithArray(true); + _testValidWithObjectWithArray(false); + } + + private void _testValidWithObjectWithArray(boolean compile) throws Exception { ObjectNode root = MAPPER.createObjectNode(); root.putArray("arr"); - ObjectNode match = root.withObject(JsonPointer.compile("/arr/2")); + ObjectNode match = compile + ? root.withObject(JsonPointer.compile("/arr/2")) + : root.withObject("/arr/2"); assertTrue(match.isObject()); match.put("value", 42); assertEquals(a2q("{'arr':[null,null,{'value':42}]}"), root.toString()); // But also verify we can match - ObjectNode match2 = root.withObject(JsonPointer.compile("/arr/2")); + ObjectNode match2 = compile + ? root.withObject(JsonPointer.compile("/arr/2")) + : root.withObject("/arr/2"); assertSame(match, match2); match2.put("value2", true); assertEquals(a2q("{'arr':[null,null,{'value':42,'value2':true}]}"), root.toString()); // And even more! `null`s can be replaced by default - ObjectNode match3 = root.withObject(JsonPointer.compile("/arr/0")); + ObjectNode match3 = compile + ? root.withObject(JsonPointer.compile("/arr/0")) + : root.withObject("/arr/0"); assertEquals("{}", match3.toString()); match3.put("value", "bar"); assertEquals(a2q("{'arr':[{'value':'bar'},null,{'value':42,'value2':true}]}"), @@ -116,7 +162,6 @@ public void testValidWithObjectWithArray() throws Exception // But not if prevented _verifyObjectReplaceFail(root, "/arr/1", OverwriteMode.NONE); - } private void _verifyObjectReplaceFail(JsonNode doc, String ptrExpr, OverwriteMode mode) { @@ -164,7 +209,12 @@ public void testValidWithArrayTrivial() throws Exception } // From Javadoc example - public void testValidWithArraySimple() throws Exception + public void testValidWithArraySimple() throws Exception { + _testValidWithArraySimple(true); + _testValidWithArraySimple(false); + } + + private void _testValidWithArraySimple(boolean compile) throws Exception { final String DOC_STR = a2q( "{'a':{'b':[1,2],'c':true}}" @@ -172,13 +222,17 @@ public void testValidWithArraySimple() throws Exception JsonNode doc = MAPPER.readTree(DOC_STR); { - ArrayNode match = doc.withArray(JsonPointer.compile("/a/b")); + ArrayNode match = compile + ? doc.withArray(JsonPointer.compile("/a/b")) + : doc.withArray("/a/b"); assertEquals(a2q("[1,2]"), match.toString()); // should not modify the doc assertEquals(DOC_STR, doc.toString()); } { - ArrayNode match = doc.withArray(JsonPointer.compile("/a/x")); + ArrayNode match = compile + ? doc.withArray(JsonPointer.compile("/a/x")) + : doc.withArray("/a/x"); assertEquals(a2q("[]"), match.toString()); // does modify the doc assertEquals(a2q( @@ -189,8 +243,9 @@ public void testValidWithArraySimple() throws Exception // then acceptable replacement { - ArrayNode match = doc.withArray(JsonPointer.compile("/a/b/0"), - OverwriteMode.ALL, true); + ArrayNode match = compile + ? doc.withArray(JsonPointer.compile("/a/b/0"), OverwriteMode.ALL, true) + : doc.withArray("/a/b/0", OverwriteMode.ALL, true); assertEquals(a2q("[]"), match.toString()); // does further modify the doc assertEquals(a2q( @@ -217,5 +272,4 @@ private void _verifyArrayReplaceFail(JsonNode doc, JsonPointer ptr, OverwriteMod verifyException(e, "(mode `OverwriteMode."+mode.name()+"`)"); } } - }