Skip to content

Commit 1bcd717

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 8a7976f commit 1bcd717

File tree

2 files changed

+66
-15
lines changed

2 files changed

+66
-15
lines changed

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

Lines changed: 37 additions & 15 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.
@@ -21,8 +21,8 @@
2121
import java.io.InputStream;
2222
import java.util.ArrayList;
2323
import java.util.Arrays;
24-
import java.util.HashMap;
2524
import java.util.Collection;
25+
import java.util.HashMap;
2626
import java.util.Iterator;
2727
import java.util.Map;
2828
import java.util.Map.Entry;
@@ -42,9 +42,9 @@
4242
import com.fasterxml.jackson.databind.JsonNode;
4343
import com.fasterxml.jackson.databind.ObjectMapper;
4444
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector;
45-
import com.fasterxml.jackson.databind.node.ArrayNode;
4645
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
4746
import com.fasterxml.jackson.databind.introspect.ClassIntrospector;
47+
import com.fasterxml.jackson.databind.node.ArrayNode;
4848
import com.fasterxml.jackson.databind.node.ObjectNode;
4949

5050
/**
@@ -208,7 +208,7 @@ private <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exc
208208
continue;
209209
}
210210

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

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

323323
if (source == null) {
324324
return;
325325
}
326326

327327
Iterator<Entry<String, JsonNode>> fields = node.fields();
328+
Class<?> keyType = typeOrObject(type.getComponentType());
329+
Class<?> valueType = typeOrObject(type.getMapValueType());
328330

329331
while (fields.hasNext()) {
330332

331333
Entry<String, JsonNode> entry = fields.next();
332-
JsonNode child = entry.getValue();
333-
Object sourceValue = source.get(entry.getKey());
334+
JsonNode value = entry.getValue();
335+
String key = entry.getKey();
334336

335-
if (child instanceof ObjectNode && sourceValue != null) {
337+
Object mappedKey = mapper.readValue(quote(key), keyType);
338+
Object sourceValue = source.get(mappedKey);
336339

337-
doMerge((ObjectNode) child, sourceValue, mapper);
340+
if (value instanceof ObjectNode && sourceValue != null) {
338341

339-
} else if (child instanceof ArrayNode && sourceValue != null) {
342+
doMerge((ObjectNode) value, sourceValue, mapper);
340343

341-
handleArray(child, sourceValue, mapper, type);
344+
} else if (value instanceof ArrayNode && sourceValue != null) {
345+
346+
handleArray(value, sourceValue, mapper, type);
342347

343348
} else {
344349

345-
Class<?> valueType = sourceValue == null ? Object.class : sourceValue.getClass();
346-
source.put(entry.getKey(), mapper.treeToValue(child, valueType));
350+
Class<?> typeToRead = sourceValue != null ? sourceValue.getClass() : valueType;
351+
source.put(mappedKey, mapper.treeToValue(value, typeToRead));
347352
}
348353

349354
fields.remove();
@@ -374,13 +379,30 @@ private static Collection<Object> ifCollection(Object source) {
374379
}
375380

376381
/**
377-
* Simple value object to capture a mapping of Jackson mapped field names and {@link PersistentProperty} instances.
382+
* Surrounds the given source {@link String} with quotes so that they represent a valid JSON String.
378383
*
379384
* @param source can be {@literal null}.
385+
* @return
386+
*/
387+
private static String quote(String source) {
388+
return source == null ? null : "\"".concat(source).concat("\"");
389+
}
390+
391+
/**
392+
* Returns the raw type of the given {@link TypeInformation} or {@link Object} as fallback.
393+
*
394+
* @param type can be {@literal null}.
395+
* @return
396+
*/
397+
private static Class<?> typeOrObject(TypeInformation<?> type) {
398+
return type == null ? Object.class : type.getType();
399+
}
400+
401+
/**
402+
* Simple value object to capture a mapping of Jackson mapped field names and {@link PersistentProperty} instances.
380403
*
381404
* @author Oliver Gierke
382405
*/
383-
@SuppressWarnings("unchecked")
384406
static class MappedProperties {
385407

386408
private static final ClassIntrospector INTROSPECTOR = new BasicClassIntrospector();

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));
@@ -444,6 +447,19 @@ public void testname() throws Exception {
444447
assertThat(mapper.treeToValue(node, Object.class), is((Object) "asd"));
445448
}
446449

450+
@Test // DATAREST-986
451+
public void readsComplexMap() throws Exception {
452+
453+
ObjectMapper mapper = new ObjectMapper();
454+
JsonNode node = mapper.readTree(
455+
"{ \"map\" : { \"en\" : { \"value\" : \"eventual\" }, \"de\" : { \"value\" : \"schlussendlich\" } } }");
456+
457+
Product result = reader.readPut((ObjectNode) node, new Product(), mapper);
458+
459+
assertThat(result.map.get(Locale.ENGLISH), is(new LocalizedValue("eventual")));
460+
assertThat(result.map.get(Locale.GERMAN), is(new LocalizedValue("schlussendlich")));
461+
}
462+
447463
@SuppressWarnings("unchecked")
448464
private static <T> T as(Object source, Class<T> type) {
449465

@@ -551,4 +567,17 @@ static class Child {
551567
static class Item {
552568
String some;
553569
}
570+
571+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
572+
static class Product {
573+
Map<Locale, LocalizedValue> map = new HashMap<Locale, LocalizedValue>();
574+
}
575+
576+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
577+
@NoArgsConstructor
578+
@AllArgsConstructor
579+
@EqualsAndHashCode
580+
static class LocalizedValue {
581+
String value;
582+
}
554583
}

0 commit comments

Comments
 (0)