Skip to content

Commit 571dd40

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 7d32f77 commit 571dd40

File tree

2 files changed

+78
-8
lines changed

2 files changed

+78
-8
lines changed

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

Lines changed: 46 additions & 8 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,10 @@ <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,
271+
index, root, mapper);
272+
273+
if (handleArray(child, it, mapper, property.getTypeInformation(), rawValues)) {
268274
i.remove();
269275
}
270276

@@ -304,6 +310,16 @@ <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
304310
return mapper.readerForUpdating(target).readValue(root);
305311
}
306312

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

321338
Collection<Object> collection = ifCollection(source);
322339

323340
if (collection == null) {
324341
return false;
325342
}
326343

327-
return execute(() -> handleArrayNode((ArrayNode) node, collection, mapper, collectionType.getComponentType()));
344+
return execute(
345+
() -> handleArrayNode((ArrayNode) node, collection, mapper, collectionType.getComponentType(), rawValues));
328346
}
329347

330348
/**
@@ -337,27 +355,47 @@ private boolean handleArray(JsonNode node, Object source, ObjectMapper mapper, T
337355
* @return whether an object merge has been applied to the {@link ArrayNode}.
338356
*/
339357
private boolean handleArrayNode(ArrayNode array, Collection<Object> collection, ObjectMapper mapper,
340-
TypeInformation<?> componentType) throws Exception {
358+
TypeInformation<?> componentType, IntFunction<Object> rawValues) throws Exception {
341359

342360
Assert.notNull(array, "ArrayNode must not be null!");
343361
Assert.notNull(collection, "Source collection must not be null!");
344362
Assert.notNull(mapper, "ObjectMapper must not be null!");
345363

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

351378
for (JsonNode jsonNode : array) {
352379

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

357395
Object next = value.next();
358396

359397
if (ArrayNode.class.isInstance(jsonNode)) {
360-
return handleArray(jsonNode, next, mapper, getTypeToMap(value, componentType));
398+
return handleArray(jsonNode, next, mapper, getTypeToMap(value, componentType), null);
361399
}
362400

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

411449
} else if (value instanceof ArrayNode && sourceValue != null) {
412450

413-
handleArray(value, sourceValue, mapper, getTypeToMap(sourceValue, typeToMap));
451+
handleArray(value, sourceValue, mapper, getTypeToMap(sourceValue, typeToMap), null);
414452

415453
} else {
416454

@@ -685,8 +723,8 @@ public void doWithPersistentProperty(PersistentProperty<?> property) {
685723
result = mergeCollections(property, sourceValue, targetValue, mapper);
686724
} else if (property.isEntity()) {
687725

688-
result = targetValue.isPresent() ? targetValue.flatMap(t -> sourceValue.map(s -> mergeForPut(s, t, mapper)))
689-
: sourceValue;
726+
result = targetValue.isPresent() ? targetValue.flatMap(t -> sourceValue.map(s -> mergeForPut(s, t, mapper)))
727+
: sourceValue;
690728
} else {
691729
result = sourceValue;
692730
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.data.mapping.context.PersistentEntities;
5050
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
5151
import org.springframework.data.rest.core.mapping.ResourceMappings;
52+
import org.springframework.data.rest.webmvc.json.DomainObjectReaderUnitTests.BugModel.NestedModel;
5253
import org.springframework.data.rest.webmvc.mapping.Associations;
5354

5455
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -113,6 +114,7 @@ void setUp() {
113114
mappingContext.getPersistentEntity(Apple.class);
114115
mappingContext.getPersistentEntity(Pear.class);
115116
mappingContext.getPersistentEntity(WithCustomMappedPrimitiveCollection.class);
117+
mappingContext.getPersistentEntity(BugModel.class);
116118
mappingContext.afterPropertiesSet();
117119

118120
this.entities = new PersistentEntities(Collections.singleton(mappingContext));
@@ -682,6 +684,26 @@ void nestedEntitiesAreCreatedWhenMissingForPut() throws Exception {
682684
assertThat(result.inner.hidden).isNull();
683685
}
684686

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

@@ -968,4 +990,14 @@ public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce
968990
}
969991
}
970992
}
993+
994+
// GH-2287
995+
static class BugModel {
996+
997+
public List<NestedModel> list;
998+
999+
static class NestedModel {
1000+
public String value;
1001+
}
1002+
}
9711003
}

0 commit comments

Comments
 (0)