From ac4b27159b3a6028704137fb5c00da6efe24f203 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 2 Jun 2011 11:47:00 +0200 Subject: [PATCH] DATADOC-161 - MappingMongoConverter now supports nested Maps as well. Extracted method to handle reading Maps which is now also able to recursively resolve nested maps by using the generics information. --- .../convert/MappingMongoConverter.java | 80 ++++++++++++------- .../MappingMongoConverterUnitTests.java | 33 ++++++++ 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java index 521b4ac77..3146d7215 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java @@ -24,10 +24,10 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; @@ -41,6 +41,7 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.support.ConversionServiceFactory; import org.springframework.data.document.mongodb.MongoDbFactory; @@ -333,9 +334,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (!dbo.containsField("_id") && null != idProperty) { Object idObj = null; Class[] targetClasses = new Class[]{ObjectId.class, Object.class}; - for (int i = 0; i < targetClasses.length; i++) { + for (Class targetClasse : targetClasses) { try { - idObj = wrapper.getProperty(idProperty, targetClasses[i], useFieldAccessOnly); + idObj = wrapper.getProperty(idProperty, targetClasse, useFieldAccessOnly); if (null != idObj) { break; } @@ -610,9 +611,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return new DBRef(db, collection, id); } - @SuppressWarnings({"unchecked"}) - protected Object getValueInternal(MongoPersistentProperty prop, DBObject dbo, StandardEvaluationContext ctx, - String spelExpr) { + + protected Object getValueInternal(MongoPersistentProperty prop, DBObject dbo, StandardEvaluationContext ctx, String spelExpr) { Object o; if (null != spelExpr) { @@ -637,27 +637,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App dbObj = ((DBRef) dbObj).fetch(); } if (dbObj instanceof DBObject) { - if (prop.isMap() && dbObj instanceof DBObject) { - - // We have to find a potentially stored class to be used first. - Class toType = findTypeToBeUsed((DBObject) dbObj); - Map m = new LinkedHashMap(); - - for (Map.Entry entry : ((Map) ((DBObject) dbObj).toMap()).entrySet()) { - if (entry.getKey().equals(CUSTOM_TYPE_KEY)) { - continue; - } - - Class keyType = prop.getComponentType(); - Object key = conversionService.convert(entry.getKey(), keyType); - - if (null != entry.getValue() && entry.getValue() instanceof DBObject) { - m.put(key, read((null != toType ? toType : prop.getMapValueType()), (DBObject) entry.getValue())); - } else { - m.put(key, entry.getValue()); - } - } - return m; + if (prop.isMap()) { + return readMap(prop.getTypeInformation(), (DBObject) dbObj); } else if (prop.isArray() && dbObj instanceof BasicDBObject && ((DBObject) dbObj).keySet().size() == 0) { // It's empty return Array.newInstance(prop.getComponentType(), 0); @@ -697,6 +678,51 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return o; } + /** + * Reads the given {@link DBObject} into a {@link Map}. will recursively resolve nested {@link Map}s as well. + * + * @param type the {@link Map} {@link TypeInformation} to be used to unmarshall this {@link DBObject}. + * @param dbObject + * + * @return + */ + @SuppressWarnings("unchecked") + private Map readMap(TypeInformation type, DBObject dbObject) { + + Assert.notNull(type); + Assert.isTrue(type.isMap()); + Assert.notNull(dbObject); + + Class customMapType = findTypeToBeUsed(dbObject); + Class mapType = customMapType == null ? Map.class : customMapType; + + Map map = CollectionFactory.createMap(mapType, dbObject.keySet().size()); + Map sourceMap = dbObject.toMap(); + + for (Entry entry : sourceMap.entrySet()) { + if (entry.getKey().equals(CUSTOM_TYPE_KEY)) { + continue; + } + + Class keyType = type.getComponentType().getType(); + Object key = conversionService.convert(entry.getKey(), keyType); + + if (null != entry.getValue() && entry.getValue() instanceof DBObject) { + + DBObject valueSource = (DBObject) entry.getValue(); + TypeInformation valueType = type.getMapValueType(); + + Object value = valueType.isMap() ? readMap(valueType, valueSource) : read(valueType, valueSource); + + map.put(key, value); + } else { + map.put(key, entry.getValue()); + } + } + + return map; + } + /** * Returns the type to be used to convert the DBObject given to. Will return {@literal null} if there's not type hint * found in the {@link DBObject} or the type hint found can't be converted into a {@link Class} as the type might not diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java index 463936946..8f6d583d3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java @@ -23,7 +23,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -285,6 +287,33 @@ public class MappingMongoConverterUnitTests { assertThat(read.locale, is(Locale.US)); } + /** + * @see DATADOC-161 + */ + @Test + public void readsNestedMapsCorrectly() { + + Map secondLevel = new HashMap(); + secondLevel.put("key1", "value1"); + secondLevel.put("key2", "value2"); + + Map> firstLevel = new HashMap>(); + firstLevel.put("level1", secondLevel); + firstLevel.put("level2", secondLevel); + + ClassWithNestedMaps maps = new ClassWithNestedMaps(); + maps.nestedMaps = new LinkedHashMap>>(); + maps.nestedMaps.put("afield", firstLevel); + + DBObject dbObject = new BasicDBObject(); + converter.write(maps, dbObject); + + ClassWithNestedMaps result = converter.read(ClassWithNestedMaps.class, dbObject); + Map>> nestedMap = result.nestedMaps; + assertThat(nestedMap, is(notNullValue())); + assertThat(nestedMap.get("afield"), is(firstLevel)); + } + class ClassWithEnumProperty { SampleEnum sampleEnum; @@ -314,6 +343,10 @@ public class MappingMongoConverterUnitTests { Map map; } + class ClassWithNestedMaps { + Map>> nestedMaps; + } + public static class BirthDateContainer { LocalDate birthDate; }