Skip to content

Commit e9526f2

Browse files
committed
DATAREST-977 - Fixed reading of complex enums on collection expansion for PATCH.
When merging collections on PATCH we now don't use the first collection item's type for all elements but inspect the values for each existing element found. When it comes to appending elements to the collection, wen now just stick to the declared component type as type hint for reading the provided value.
1 parent 86af638 commit e9526f2

File tree

2 files changed

+78
-14
lines changed

2 files changed

+78
-14
lines changed

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

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,7 @@ private boolean handleArray(JsonNode node, Object source, ObjectMapper mapper, T
347347
return false;
348348
}
349349

350-
Iterator<Object> iterator = collection.iterator();
351-
TypeInformation<?> componentType = iterator.hasNext() ? //
352-
ClassTypeInformation.from(iterator.next().getClass()) : //
353-
collectionType.getComponentType();
354-
355-
return handleArrayNode((ArrayNode) node, collection, mapper, componentType);
350+
return handleArrayNode((ArrayNode) node, collection, mapper, collectionType.getComponentType());
356351
}
357352

358353
/**
@@ -380,16 +375,15 @@ private boolean handleArrayNode(ArrayNode array, Collection<Object> collection,
380375

381376
if (!value.hasNext()) {
382377

383-
Class<?> type = componentType == null ? Object.class : componentType.getType();
384-
collection.add(mapper.treeToValue(jsonNode, type));
378+
collection.add(mapper.treeToValue(jsonNode, getTypeToMap(null, componentType).getType()));
385379

386380
continue;
387381
}
388382

389383
Object next = value.next();
390384

391385
if (ArrayNode.class.isInstance(jsonNode)) {
392-
return handleArray(jsonNode, next, mapper, componentType);
386+
return handleArray(jsonNode, next, mapper, getTypeToMap(value, componentType));
393387
}
394388

395389
if (ObjectNode.class.isInstance(jsonNode)) {
@@ -424,7 +418,7 @@ private void doMergeNestedMap(Map<Object, Object> source, ObjectNode node, Objec
424418

425419
Iterator<Entry<String, JsonNode>> fields = node.fields();
426420
Class<?> keyType = typeOrObject(type.getComponentType());
427-
Class<?> valueType = typeOrObject(type.getMapValueType());
421+
TypeInformation<?> valueType = type.getMapValueType();
428422

429423
while (fields.hasNext()) {
430424

@@ -434,19 +428,19 @@ private void doMergeNestedMap(Map<Object, Object> source, ObjectNode node, Objec
434428

435429
Object mappedKey = mapper.readValue(quote(key), keyType);
436430
Object sourceValue = source.get(mappedKey);
431+
TypeInformation<?> typeToMap = getTypeToMap(sourceValue, valueType);
437432

438433
if (value instanceof ObjectNode && sourceValue != null) {
439434

440435
doMerge((ObjectNode) value, sourceValue, mapper);
441436

442437
} else if (value instanceof ArrayNode && sourceValue != null) {
443438

444-
handleArray(value, sourceValue, mapper, type);
439+
handleArray(value, sourceValue, mapper, getTypeToMap(sourceValue, typeToMap));
445440

446441
} else {
447442

448-
Class<?> typeToRead = sourceValue != null ? sourceValue.getClass() : valueType;
449-
source.put(mappedKey, mapper.treeToValue(value, typeToRead));
443+
source.put(mappedKey, mapper.treeToValue(value, typeToMap.getType()));
450444
}
451445

452446
fields.remove();
@@ -562,6 +556,32 @@ private static Class<?> typeOrObject(TypeInformation<?> type) {
562556
return type == null ? Object.class : type.getType();
563557
}
564558

559+
/**
560+
* Returns the type to read for the given value and default type. The type will be defaulted to {@link Object} if
561+
* missing. If the given value's type is different from the given default (i.e. more concrete) the value's type will
562+
* be used.
563+
*
564+
* @param value can be {@literal null}.
565+
* @param type can be {@literal null}.
566+
* @return
567+
*/
568+
private static TypeInformation<?> getTypeToMap(Object value, TypeInformation<?> type) {
569+
570+
if (type == null) {
571+
type = ClassTypeInformation.OBJECT;
572+
}
573+
574+
if (value == null) {
575+
return type;
576+
}
577+
578+
if (Enum.class.isInstance(value)) {
579+
return ClassTypeInformation.from(((Enum<?>) value).getDeclaringClass());
580+
}
581+
582+
return value.getClass().equals(type.getType()) ? type : ClassTypeInformation.from(value.getClass());
583+
}
584+
565585
/**
566586
* Simple value object to capture a mapping of Jackson mapped field names and {@link PersistentProperty} instances.
567587
*

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

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

18-
import static org.hamcrest.CoreMatchers.*;
18+
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
2020
import static org.mockito.Mockito.*;
2121

@@ -93,6 +93,7 @@ public void setUp() {
9393
mappingContext.getPersistentEntity(Parent.class);
9494
mappingContext.getPersistentEntity(Product.class);
9595
mappingContext.getPersistentEntity(TransientReadOnlyProperty.class);
96+
mappingContext.getPersistentEntity(CollectionOfEnumWithMethods.class);
9697
mappingContext.afterPropertiesSet();
9798

9899
PersistentEntities entities = new PersistentEntities(Collections.singleton(mappingContext));
@@ -499,6 +500,20 @@ public void handlesTransientPropertyWithoutFieldProperly() throws Exception {
499500
reader.readPut((ObjectNode) node, new TransientReadOnlyProperty(), mapper);
500501
}
501502

503+
@Test // DATAREST-977
504+
public void readsCollectionOfComplexEnum() throws Exception {
505+
506+
CollectionOfEnumWithMethods sample = new CollectionOfEnumWithMethods();
507+
sample.enums.add(SampleEnum.FIRST);
508+
509+
ObjectMapper mapper = new ObjectMapper();
510+
JsonNode node = mapper.readTree("{ \"enums\" : [ \"SECOND\", \"FIRST\" ] }");
511+
512+
CollectionOfEnumWithMethods result = reader.merge((ObjectNode) node, sample, mapper);
513+
514+
assertThat(result.enums, contains(SampleEnum.SECOND, SampleEnum.FIRST));
515+
}
516+
502517
@SuppressWarnings("unchecked")
503518
private static <T> T as(Object source, Class<T> type) {
504519

@@ -635,4 +650,33 @@ public String getName() {
635650

636651
public void setName(String name) {}
637652
}
653+
654+
// DATAREST-977
655+
656+
interface EnumInterface {
657+
String getFoo();
658+
}
659+
660+
static enum SampleEnum implements EnumInterface {
661+
662+
FIRST {
663+
664+
@Override
665+
public String getFoo() {
666+
return "first";
667+
}
668+
669+
},
670+
SECOND {
671+
672+
public String getFoo() {
673+
return "second";
674+
}
675+
};
676+
}
677+
678+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
679+
static class CollectionOfEnumWithMethods {
680+
List<SampleEnum> enums = new ArrayList<SampleEnum>();
681+
}
638682
}

0 commit comments

Comments
 (0)