Skip to content

Commit a567a89

Browse files
committed
Reinstate new object addition for collections in patch operations.
The removal of manual collection value appendance for primitives in the context of GH-2261 lead to complex object to be appended onto a collection not being deserialized at all. This deserialization is now reinstantiated with the value to be added looked up by reading the parent node into the collections element type using a JSON Pointer expression of /$propertyName/$index. This is done to make sure that @JsonDeserialize annotations on the property kick in for the deserialization of the individual elements. We now also shortcut the entire merge algorithm for collections that are empty, contain primitives or enums. Fixes GH-2287.
1 parent 48590cf commit a567a89

File tree

2 files changed

+76
-6
lines changed

2 files changed

+76
-6
lines changed

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.rest.webmvc.json;
1717

18+
import java.io.IOException;
1819
import java.io.InputStream;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
@@ -24,6 +25,7 @@
2425
import java.util.Map;
2526
import java.util.Map.Entry;
2627
import java.util.Optional;
28+
import java.util.function.IntFunction;
2729

2830
import org.springframework.beans.PropertyAccessor;
2931
import org.springframework.beans.PropertyAccessorFactory;
@@ -44,6 +46,7 @@
4446
import org.springframework.http.converter.HttpMessageNotReadableException;
4547
import org.springframework.lang.Nullable;
4648
import org.springframework.util.Assert;
49+
import org.springframework.util.ClassUtils;
4750
import org.springframework.util.ObjectUtils;
4851

4952
import com.fasterxml.jackson.databind.JsonNode;
@@ -264,7 +267,11 @@ <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
264267

265268
if (child.isArray()) {
266269

267-
if (handleArray(child, it, mapper, property.getTypeInformation())) {
270+
IntFunction<Object> rawValues = index -> readRawCollectionElement(property.getComponentType(), fieldName, index,
271+
root,
272+
mapper);
273+
274+
if (handleArray(child, it, mapper, property.getTypeInformation(), rawValues)) {
268275
i.remove();
269276
}
270277

@@ -304,6 +311,16 @@ <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
304311
return mapper.readerForUpdating(target).readValue(root);
305312
}
306313

314+
private static Object readRawCollectionElement(Class<?> elementType, String fieldName, int index, JsonNode root,
315+
ObjectMapper mapper) {
316+
317+
try {
318+
return mapper.readerFor(elementType).at("/" + fieldName + "/" + index).readValue(root);
319+
} catch (IOException o_O) {
320+
throw new RuntimeException(o_O);
321+
}
322+
}
323+
307324
/**
308325
* Handles the given {@link JsonNode} by treating it as {@link ArrayNode} and the given source value as
309326
* {@link Collection}-like value. Looks up the actual type to handle from the potentially available first element,
@@ -316,15 +333,17 @@ <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
316333
* @return
317334
* @throws Exception
318335
*/
319-
private boolean handleArray(JsonNode node, Object source, ObjectMapper mapper, TypeInformation<?> collectionType) {
336+
private boolean handleArray(JsonNode node, Object source, ObjectMapper mapper, TypeInformation<?> collectionType,
337+
IntFunction<Object> rawValues) {
320338

321339
Collection<Object> collection = ifCollection(source);
322340

323341
if (collection == null) {
324342
return false;
325343
}
326344

327-
return execute(() -> handleArrayNode((ArrayNode) node, collection, mapper, collectionType.getComponentType()));
345+
return execute(
346+
() -> handleArrayNode((ArrayNode) node, collection, mapper, collectionType.getComponentType(), rawValues));
328347
}
329348

330349
/**
@@ -337,27 +356,47 @@ private boolean handleArray(JsonNode node, Object source, ObjectMapper mapper, T
337356
* @return whether an object merge has been applied to the {@link ArrayNode}.
338357
*/
339358
private boolean handleArrayNode(ArrayNode array, Collection<Object> collection, ObjectMapper mapper,
340-
TypeInformation<?> componentType) throws Exception {
359+
TypeInformation<?> componentType, IntFunction<Object> rawValues) throws Exception {
341360

342361
Assert.notNull(array, "ArrayNode must not be null");
343362
Assert.notNull(collection, "Source collection must not be null");
344363
Assert.notNull(mapper, "ObjectMapper must not be null");
345364

365+
// Empty collection? Primitive? Enum? No need to merge.
366+
if (array.isEmpty()
367+
|| collection.isEmpty()
368+
|| ClassUtils.isPrimitiveOrWrapper(componentType.getType())
369+
|| componentType.getType().isEnum()) {
370+
return false;
371+
}
372+
346373
// We need an iterator for the original collection.
347374
// We might modify it but we want to keep iterating over the original collection.
348375
Iterator<Object> value = new ArrayList<Object>(collection).iterator();
349376
boolean nestedObjectFound = false;
377+
int i = 0;
350378

351379
for (JsonNode jsonNode : array) {
352380

381+
int current = i++;
382+
383+
// We need to append new elements
353384
if (!value.hasNext()) {
385+
386+
nestedObjectFound = true;
387+
388+
// Use pre-read values if available. Deserialize node otherwise.
389+
collection.add(rawValues != null
390+
? rawValues.apply(current)
391+
: mapper.treeToValue(jsonNode, componentType.getType()));
392+
354393
break;
355394
}
356395

357396
Object next = value.next();
358397

359398
if (ArrayNode.class.isInstance(jsonNode)) {
360-
return handleArray(jsonNode, next, mapper, getTypeToMap(value, componentType));
399+
return handleArray(jsonNode, next, mapper, getTypeToMap(value, componentType), null);
361400
}
362401

363402
if (ObjectNode.class.isInstance(jsonNode)) {
@@ -410,7 +449,7 @@ private void doMergeNestedMap(Map<Object, Object> source, ObjectNode node, Objec
410449

411450
} else if (value instanceof ArrayNode && sourceValue != null) {
412451

413-
handleArray(value, sourceValue, mapper, getTypeToMap(sourceValue, typeToMap));
452+
handleArray(value, sourceValue, mapper, getTypeToMap(sourceValue, typeToMap), null);
414453

415454
} else {
416455

spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ void setUp() {
113113
mappingContext.getPersistentEntity(Apple.class);
114114
mappingContext.getPersistentEntity(Pear.class);
115115
mappingContext.getPersistentEntity(WithCustomMappedPrimitiveCollection.class);
116+
mappingContext.getPersistentEntity(BugModel.class);
116117
mappingContext.afterPropertiesSet();
117118

118119
this.entities = new PersistentEntities(Collections.singleton(mappingContext));
@@ -684,6 +685,26 @@ void nestedEntitiesAreCreatedWhenMissingForPut() throws Exception {
684685
assertThat(result.inner.hidden).isNull();
685686
}
686687

688+
@Test
689+
void deserializesNewNestedEntitiesCorrectly() throws Exception {
690+
691+
var mapper = new ObjectMapper();
692+
var node = mapper.readTree("{ \"list\" : [ { \"value\" : \"Foo\" }, { \"value\" : \"Bar\" }] }");
693+
694+
var nested = new BugModel.NestedModel();
695+
nested.value = "FooBar";
696+
697+
var model = new BugModel();
698+
model.list = new ArrayList<>();
699+
model.list.add(nested);
700+
701+
var result = reader.doMerge((ObjectNode) node, model, mapper);
702+
703+
assertThat(result.list)
704+
.extracting(it -> it.value)
705+
.containsExactly("Foo", "Bar");
706+
}
707+
687708
@SuppressWarnings("unchecked")
688709
private static <T> T as(Object source, Class<T> type) {
689710

@@ -966,4 +987,14 @@ public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce
966987
}
967988
}
968989
}
990+
991+
// GH-2287
992+
static class BugModel {
993+
994+
public List<NestedModel> list;
995+
996+
static class NestedModel {
997+
public String value;
998+
}
999+
}
9691000
}

0 commit comments

Comments
 (0)