Skip to content

Commit 96ff0cb

Browse files
committed
DATAREST-986 - DomainObjectReader now merges complex nested maps correctly.
We now use a Map property's generic type information to make sure we convert both the key and the value into the declared types. Previously we just used Object if the source value to map was null. Object is still used as fallback for raw maps though.
1 parent 2b9c784 commit 96ff0cb

File tree

2 files changed

+65
-11
lines changed

2 files changed

+65
-11
lines changed

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

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2016 the original author or authors.
2+
* Copyright 2014-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -204,7 +204,7 @@ private <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exc
204204
continue;
205205
}
206206

207-
doMergeNestedMap((Map<String, Object>) rawValue, objectNode, mapper, property.getTypeInformation());
207+
doMergeNestedMap((Map<Object, Object>) rawValue, objectNode, mapper, property.getTypeInformation());
208208

209209
// Remove potentially emptied Map as values have been handled recursively
210210
if (!objectNode.fieldNames().hasNext()) {
@@ -313,33 +313,38 @@ private boolean handleArrayNode(ArrayNode array, Collection<Object> collection,
313313
* @param mapper must not be {@literal null}.
314314
* @throws Exception
315315
*/
316-
private void doMergeNestedMap(Map<String, Object> source, ObjectNode node, ObjectMapper mapper,
316+
private void doMergeNestedMap(Map<Object, Object> source, ObjectNode node, ObjectMapper mapper,
317317
TypeInformation<?> type) throws Exception {
318318

319319
if (source == null) {
320320
return;
321321
}
322322

323323
Iterator<Entry<String, JsonNode>> fields = node.fields();
324+
Class<?> keyType = typeOrObject(type.getComponentType());
325+
Class<?> valueType = typeOrObject(type.getMapValueType());
324326

325327
while (fields.hasNext()) {
326328

327329
Entry<String, JsonNode> entry = fields.next();
328-
JsonNode child = entry.getValue();
329-
Object sourceValue = source.get(entry.getKey());
330+
JsonNode value = entry.getValue();
331+
String key = entry.getKey();
332+
333+
Object mappedKey = mapper.readValue(quote(key), keyType);
334+
Object sourceValue = source.get(mappedKey);
330335

331-
if (child instanceof ObjectNode && sourceValue != null) {
336+
if (value instanceof ObjectNode && sourceValue != null) {
332337

333-
doMerge((ObjectNode) child, sourceValue, mapper);
338+
doMerge((ObjectNode) value, sourceValue, mapper);
334339

335-
} else if (child instanceof ArrayNode && sourceValue != null) {
340+
} else if (value instanceof ArrayNode && sourceValue != null) {
336341

337-
handleArray(child, sourceValue, mapper, type);
342+
handleArray(value, sourceValue, mapper, type);
338343

339344
} else {
340345

341-
Class<?> valueType = sourceValue == null ? Object.class : sourceValue.getClass();
342-
source.put(entry.getKey(), mapper.treeToValue(child, valueType));
346+
Class<?> typeToRead = sourceValue != null ? sourceValue.getClass() : valueType;
347+
source.put(mappedKey, mapper.treeToValue(value, typeToRead));
343348
}
344349

345350
fields.remove();
@@ -368,4 +373,24 @@ private static Collection<Object> ifCollection(Object source) {
368373

369374
return null;
370375
}
376+
377+
/**
378+
* Surrounds the given source {@link String} with quotes so that they represent a valid JSON String.
379+
*
380+
* @param source can be {@literal null}.
381+
* @return
382+
*/
383+
private static String quote(String source) {
384+
return source == null ? null : "\"".concat(source).concat("\"");
385+
}
386+
387+
/**
388+
* Returns the raw type of the given {@link TypeInformation} or {@link Object} as fallback.
389+
*
390+
* @param type can be {@literal null}.
391+
* @return
392+
*/
393+
private static Class<?> typeOrObject(TypeInformation<?> type) {
394+
return type == null ? Object.class : type.getType();
395+
}
371396
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.mockito.Mockito.*;
2121

2222
import lombok.AllArgsConstructor;
23+
import lombok.EqualsAndHashCode;
2324
import lombok.NoArgsConstructor;
2425

2526
import java.io.ByteArrayInputStream;
@@ -33,6 +34,7 @@
3334
import java.util.HashMap;
3435
import java.util.Iterator;
3536
import java.util.List;
37+
import java.util.Locale;
3638
import java.util.Map;
3739

3840
import org.junit.Before;
@@ -88,6 +90,7 @@ public void setUp() {
8890
mappingContext.getPersistentEntity(Inner.class);
8991
mappingContext.getPersistentEntity(Outer.class);
9092
mappingContext.getPersistentEntity(Parent.class);
93+
mappingContext.getPersistentEntity(Product.class);
9194
mappingContext.afterPropertiesSet();
9295

9396
PersistentEntities entities = new PersistentEntities(Collections.singleton(mappingContext));
@@ -396,6 +399,19 @@ public void testname() throws Exception {
396399
assertThat(mapper.treeToValue(node, Object.class), is((Object) "asd"));
397400
}
398401

402+
@Test // DATAREST-986
403+
public void readsComplexMap() throws Exception {
404+
405+
ObjectMapper mapper = new ObjectMapper();
406+
JsonNode node = mapper.readTree(
407+
"{ \"map\" : { \"en\" : { \"value\" : \"eventual\" }, \"de\" : { \"value\" : \"schlussendlich\" } } }");
408+
409+
Product result = reader.readPut((ObjectNode) node, new Product(), mapper);
410+
411+
assertThat(result.map.get(Locale.ENGLISH), is(new LocalizedValue("eventual")));
412+
assertThat(result.map.get(Locale.GERMAN), is(new LocalizedValue("schlussendlich")));
413+
}
414+
399415
@SuppressWarnings("unchecked")
400416
private static <T> T as(Object source, Class<T> type) {
401417

@@ -501,4 +517,17 @@ static class Child {
501517
static class Item {
502518
String some;
503519
}
520+
521+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
522+
static class Product {
523+
Map<Locale, LocalizedValue> map = new HashMap<Locale, LocalizedValue>();
524+
}
525+
526+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
527+
@NoArgsConstructor
528+
@AllArgsConstructor
529+
@EqualsAndHashCode
530+
static class LocalizedValue {
531+
String value;
532+
}
504533
}

0 commit comments

Comments
 (0)