Skip to content

Commit 94bfab0

Browse files
committed
Fix #1980 (JsonPointer.withObject()/withArray())
1 parent 0241fc4 commit 94bfab0

File tree

6 files changed

+387
-36
lines changed

6 files changed

+387
-36
lines changed

release-notes/VERSION-2.x

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Project: jackson-databind
66

77
2.14.0 (not yet released)
88

9+
#1980: Add method(s) in `JsonNode` that works like combination of `at()`
10+
and `with()`: `withObject(...)` and `withArray(...)`
911
#2541: Cannot merge polymorphic objects
1012
(reported by Matthew A)
1113
(fix contributed by James W)

src/main/java/com/fasterxml/jackson/databind/JsonNode.java

+166-17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.*;
77

88
import com.fasterxml.jackson.core.*;
9+
import com.fasterxml.jackson.databind.node.ArrayNode;
910
import com.fasterxml.jackson.databind.node.JsonNodeType;
1011
import com.fasterxml.jackson.databind.node.MissingNode;
1112
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -1124,10 +1125,16 @@ public final List<JsonNode> findParents(String fieldName)
11241125
* If the node method is called on is not Object node,
11251126
* or if property exists and has value that is not Object node,
11261127
* {@link UnsupportedOperationException} is thrown
1128+
*
1129+
* @param propertyName Name of property for the {@link ObjectNode}
1130+
*
1131+
* @return {@link ObjectNode} found or created
1132+
*
1133+
* @since 2.14
11271134
*/
1128-
public <T extends JsonNode> T withObject(String propertyName) {
1129-
throw new UnsupportedOperationException("JsonNode not of type ObjectNode (but "
1130-
+getClass().getName()+"), cannot call withObject() on it");
1135+
public ObjectNode withObject(String propertyName) {
1136+
throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but "
1137+
+getClass().getName()+"), cannot call `withObject()` on it");
11311138
}
11321139

11331140
/**
@@ -1137,10 +1144,10 @@ public <T extends JsonNode> T withObject(String propertyName) {
11371144
* consider {@link JsonPointer} segments index if at all possible
11381145
* and only secondarily as property name
11391146
*
1140-
* @param ptr Pointer that indicates path to use for Object value to return
1147+
* @param ptr {@link JsonPointer} that indicates path to use for Object value to return
11411148
* (potentially creating as necessary)
11421149
*
1143-
* @return ObjectNode found or created
1150+
* @return {@link ObjectNode} found or created
11441151
*
11451152
* @since 2.14
11461153
*/
@@ -1149,18 +1156,70 @@ public final ObjectNode withObject(JsonPointer ptr) {
11491156
}
11501157

11511158
/**
1152-
* Method that can be called on Object nodes, to access a Object-valued
1159+
* Method that can be called on Object or Array nodes, to access a Object-valued
11531160
* node pointed to by given {@link JsonPointer}, if such a node exists:
1154-
* if not, an attempt is made to create it.
1155-
* If the node method is called on is not Object node,
1156-
* or if property exists and has value that is not Object node,
1157-
* {@link UnsupportedOperationException} is thrown
1161+
* or if not, an attempt is made to create one and return it.
1162+
* For example, on document
1163+
*<pre>
1164+
* { "a" : {
1165+
* "b" : {
1166+
* "c" : 13
1167+
* }
1168+
* }
1169+
* }
1170+
*</pre>
1171+
* calling method with {@link JsonPointer} of {@code /a/b} would return
1172+
* {@link ObjectNode}
1173+
*<pre>
1174+
* { "c" : 13 }
1175+
*</pre>
1176+
*<p>
1177+
* In cases where path leads to "missing" nodes, a path is created.
1178+
* So, for example, on above document, and
1179+
* {@link JsonPointer} of {@code /a/x} an empty {@link ObjectNode} would
1180+
* be returned and the document would look like:
1181+
*<pre>
1182+
* { "a" : {
1183+
* "b" : {
1184+
* "c" : 13
1185+
* },
1186+
* "x" : { }
1187+
* }
1188+
* }
1189+
*</pre>
1190+
* Finally, if the path is incompatible with the document -- there is an existing
1191+
* {@code JsonNode} through which expression cannot go -- a replacement is
1192+
* attempted if (and only if) conversion is allowed as per {@code overwriteMode}
1193+
* passed in. For example, with above document and expression of {@code /a/b/c},
1194+
* conversion is allowed if passing {@code OverwriteMode.SCALARS} or
1195+
* {@code OvewriteMode.ALL}, and resulting document would look like:
1196+
*<pre>
1197+
* { "a" : {
1198+
* "b" : {
1199+
* "c" : { }
1200+
* },
1201+
* "x" : { }
1202+
* }
1203+
* }
1204+
*</pre>
1205+
* but if different modes ({@code NONE} or {@code NULLS}) is passed, an exception
1206+
* is thrown instead.
11581207
*
1159-
* @param ptr Pointer that indicates path to use for Object value to return
1160-
* (potentially creating as necessary)
1161-
* @param overwriteMode Defines w
1208+
* @param ptr Pointer that indicates path to use for {@link ObjectNode} value to return
1209+
* (potentially creating one as necessary)
1210+
* @param overwriteMode Defines which node types may be converted in case of
1211+
* incompatible {@code JsonPointer} expression: if conversion not allowed,
1212+
* {@link UnsupportedOperationException} is thrown.
1213+
* @param preferIndex When creating a path (for empty or replacement), and path
1214+
* contains segment that may be an array index (simple integer number like
1215+
* {@code 3}), whether to construct an {@link ArrayNode} ({@code true}) or
1216+
* {@link ObjectNode} ({@code false}). In latter case matching property with
1217+
* quoted number (like {@code "3"}) is used within Object.
11621218
*
1163-
* @return ObjectNode found or created
1219+
* @return {@link ObjectNode} found or created
1220+
*
1221+
* @throws UnsupportedOperationException if a conversion would be needed for given
1222+
* {@code JsonPointer}, document, but was not allowed for the type encountered
11641223
*
11651224
* @since 2.14
11661225
*/
@@ -1172,26 +1231,116 @@ public ObjectNode withObject(JsonPointer ptr,
11721231
}
11731232

11741233
/**
1175-
* @deprecated Since 2.14 use {@code withObject} instead
1234+
* @deprecated Since 2.14 use {@code withObject(String)} instead
11761235
*/
1236+
@SuppressWarnings("unchecked")
11771237
@Deprecated // since 2.14
11781238
public final <T extends JsonNode> T with(String propertyName) {
1179-
return withObject(propertyName);
1239+
return (T) withObject(propertyName);
11801240
}
11811241

11821242
/**
1183-
* Method that can be called on Object nodes, to access a property
1243+
* Method that can be called on {@link ObjectNode} nodes, to access a property
11841244
* that has <code>Array</code> value; or if no such property exists, to create,
11851245
* add and return such Array node.
11861246
* If the node method is called on is not Object node,
11871247
* or if property exists and has value that is not Array node,
11881248
* {@link UnsupportedOperationException} is thrown
1249+
*
1250+
* @param propertyName Name of property for the {@link ArrayNode}
1251+
*
1252+
* @return {@link ArrayNode} found or created
11891253
*/
11901254
public <T extends JsonNode> T withArray(String propertyName) {
11911255
throw new UnsupportedOperationException("JsonNode not of type ObjectNode (but "
11921256
+getClass().getName()+"), cannot call withArray() on it");
11931257
}
11941258

1259+
/**
1260+
* Same as {@link #withArray(JsonPointer, OverwriteMode, boolean)} but
1261+
* with defaults of {@code OvewriteMode#NULLS} (overwrite mode)
1262+
* and {@code true} for {@code preferIndex}.
1263+
*
1264+
* @param ptr Pointer that indicates path to use for {@link ArrayNode} to return
1265+
* (potentially creating as necessary)
1266+
*
1267+
* @return {@link ArrayNode} found or created
1268+
*
1269+
* @since 2.14
1270+
*/
1271+
public final ArrayNode withArray(JsonPointer ptr) {
1272+
return withArray(ptr, OverwriteMode.NULLS, true);
1273+
}
1274+
1275+
/**
1276+
* Method that can be called on Object or Array nodes, to access a Array-valued
1277+
* node pointed to by given {@link JsonPointer}, if such a node exists:
1278+
* or if not, an attempt is made to create one and return it.
1279+
* For example, on document
1280+
*<pre>
1281+
* { "a" : {
1282+
* "b" : [ 1, 2 ]
1283+
* }
1284+
* }
1285+
*</pre>
1286+
* calling method with {@link JsonPointer} of {@code /a/b} would return
1287+
* {@code Array}
1288+
*<pre>
1289+
* [ 1, 2 ]
1290+
*</pre>
1291+
*<p>
1292+
* In cases where path leads to "missing" nodes, a path is created.
1293+
* So, for example, on above document, and
1294+
* {@link JsonPointer} of {@code /a/x} an empty {@code ArrayNode} would
1295+
* be returned and the document would look like:
1296+
*<pre>
1297+
* { "a" : {
1298+
* "b" : [ 1, 2 ],
1299+
* "x" : [ ]
1300+
* }
1301+
* }
1302+
*</pre>
1303+
* Finally, if the path is incompatible with the document -- there is an existing
1304+
* {@code JsonNode} through which expression cannot go -- a replacement is
1305+
* attempted if (and only if) conversion is allowed as per {@code overwriteMode}
1306+
* passed in. For example, with above document and expression of {@code /a/b/0},
1307+
* conversion is allowed if passing {@code OverwriteMode.SCALARS} or
1308+
* {@code OvewriteMode.ALL}, and resulting document would look like:
1309+
*<pre>
1310+
* { "a" : {
1311+
* "b" : [ [ ], 2 ],
1312+
* "x" : [ ]
1313+
* }
1314+
* }
1315+
*</pre>
1316+
* but if different modes ({@code NONE} or {@code NULLS}) is passed, an exception
1317+
* is thrown instead.
1318+
*
1319+
* @param ptr Pointer that indicates path to use for {@link ArrayNode} value to return
1320+
* (potentially creating it as necessary)
1321+
* @param overwriteMode Defines which node types may be converted in case of
1322+
* incompatible {@code JsonPointer} expression: if conversion not allowed,
1323+
* an exception is thrown.
1324+
* @param preferIndex When creating a path (for empty or replacement), and path
1325+
* contains segment that may be an array index (simple integer number like
1326+
* {@code 3}), whether to construct an {@link ArrayNode} ({@code true}) or
1327+
* {@link ObjectNode} ({@code false}). In latter case matching property with
1328+
* quoted number (like {@code "3"}) is used within Object.
1329+
*
1330+
* @return {@link ArrayNode} found or created
1331+
*
1332+
* @throws UnsupportedOperationException if a conversion would be needed for given
1333+
* {@code JsonPointer}, document, but was not allowed for the type encountered
1334+
*
1335+
* @since 2.14
1336+
*/
1337+
public ArrayNode withArray(JsonPointer ptr,
1338+
OverwriteMode overwriteMode, boolean preferIndex) {
1339+
// To avoid abstract method, base implementation just fails
1340+
throw new UnsupportedOperationException("`withArray(JsonPointer)` not implemented by "
1341+
+getClass().getName());
1342+
}
1343+
11951344
/*
11961345
/**********************************************************
11971346
/* Public API, comparison

src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java

+57-7
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ public ArrayNode deepCopy()
6565
return ret;
6666
}
6767

68+
/*
69+
/**********************************************************
70+
/* Support for withArray()/withObject()
71+
/**********************************************************
72+
*/
73+
6874
@Override
6975
protected ObjectNode _withObject(JsonPointer origPtr,
7076
JsonPointer currentPtr,
@@ -83,14 +89,35 @@ protected ObjectNode _withObject(JsonPointer origPtr,
8389
return found;
8490
}
8591
// Ok no; must replace if allowed to
86-
if (!_withObjectMayReplace(n, overwriteMode)) {
87-
_withObjectVerifyReplace(origPtr, currentPtr, overwriteMode, preferIndex, n);
88-
}
92+
_withXxxVerifyReplace(origPtr, currentPtr, overwriteMode, preferIndex, n);
8993
}
9094
// Either way; must replace or add a new property
9195
return _withObjectAddTailElement(currentPtr, preferIndex);
9296
}
9397

98+
@Override
99+
protected ArrayNode _withArray(JsonPointer origPtr,
100+
JsonPointer currentPtr,
101+
OverwriteMode overwriteMode, boolean preferIndex)
102+
{
103+
if (currentPtr.matches()) {
104+
return this;
105+
}
106+
JsonNode n = _at(currentPtr);
107+
// If there's a path, follow it
108+
if ((n != null) && (n instanceof BaseJsonNode)) {
109+
ArrayNode found = ((BaseJsonNode) n)._withArray(origPtr, currentPtr.tail(),
110+
overwriteMode, preferIndex);
111+
if (found != null) {
112+
return found;
113+
}
114+
// Ok no; must replace if allowed to
115+
_withXxxVerifyReplace(origPtr, currentPtr, overwriteMode, preferIndex, n);
116+
}
117+
// Either way; must replace or add a new property
118+
return _withArrayAddTailElement(currentPtr, preferIndex);
119+
}
120+
94121
protected ObjectNode _withObjectAddTailElement(JsonPointer tail, boolean preferIndex)
95122
{
96123
final int index = tail.getMatchingIndex();
@@ -99,22 +126,45 @@ protected ObjectNode _withObjectAddTailElement(JsonPointer tail, boolean preferI
99126
// First: did we complete traversal? If so, easy, we got our result
100127
if (tail.matches()) {
101128
ObjectNode result = this.objectNode();
102-
_withObjectSetArrayElement(index, result);
129+
_withXxxSetArrayElement(index, result);
103130
return result;
104131
}
105132

106133
// Otherwise, do we want Array or Object
107134
if (preferIndex && tail.mayMatchElement()) { // array!
108135
ArrayNode next = this.arrayNode();
109-
_withObjectSetArrayElement(index, next);
136+
_withXxxSetArrayElement(index, next);
110137
return next._withObjectAddTailElement(tail, preferIndex);
111138
}
112139
ObjectNode next = this.objectNode();
113-
_withObjectSetArrayElement(index, next);
140+
_withXxxSetArrayElement(index, next);
114141
return next._withObjectAddTailProperty(tail, preferIndex);
115142
}
116143

117-
protected void _withObjectSetArrayElement(int index, JsonNode value) {
144+
protected ArrayNode _withArrayAddTailElement(JsonPointer tail, boolean preferIndex)
145+
{
146+
final int index = tail.getMatchingIndex();
147+
tail = tail.tail();
148+
149+
// First: did we complete traversal? If so, easy, we got our result
150+
if (tail.matches()) {
151+
ArrayNode result = this.arrayNode();
152+
_withXxxSetArrayElement(index, result);
153+
return result;
154+
}
155+
156+
// Otherwise, do we want Array or Object
157+
if (preferIndex && tail.mayMatchElement()) { // array!
158+
ArrayNode next = this.arrayNode();
159+
_withXxxSetArrayElement(index, next);
160+
return next._withArrayAddTailElement(tail, preferIndex);
161+
}
162+
ArrayNode next = this.arrayNode();
163+
_withXxxSetArrayElement(index, next);
164+
return next._withArrayAddTailElement(tail, preferIndex);
165+
}
166+
167+
protected void _withXxxSetArrayElement(int index, JsonNode value) {
118168
// 27-Jul-2022, tatu: Let's make it less likely anyone OOMs by
119169
// humongous index...
120170
if (index >= size()) {

0 commit comments

Comments
 (0)