From 92079ca2009c18020b4b1ade0092c1396c8634b6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 26 Feb 2021 10:46:01 +0100 Subject: [PATCH] Introduce ConversionContext. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a ConversionContext used during the mapping process to carry forward required information. ConversionContext serves as entrpoint for recursive (read) conversion of documents, lists, maps, and simple values. The actual decision which converter strategy to apply is now encapsulated by ConversionContext.convert(…) which removes strategy duplications from the actual conversion methods. Also, converter methods for documents, maps, lists, … are now protected for easier customization by subclasses. Closes #3571 Original Pull Request: #3575 --- .../core/convert/DocumentAccessor.java | 1 + .../data/mongodb/core/convert/MapUtils.java | 111 +++ .../core/convert/MappingMongoConverter.java | 935 ++++++++++-------- .../DbRefMappingMongoConverterUnitTests.java | 2 +- .../MappingMongoConverterUnitTests.java | 5 +- 5 files changed, 613 insertions(+), 441 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MapUtils.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java index 53dd03f06..ee29fea50 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java @@ -154,6 +154,7 @@ class DocumentAccessor { * @param entity must not be {@literal null}. * @return */ + @Nullable public Object getRawId(MongoPersistentEntity entity) { return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.asMap(document).get("_id"); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MapUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MapUtils.java new file mode 100644 index 000000000..75bc72bea --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MapUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.convert; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.bson.Document; +import org.bson.conversions.Bson; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +import com.mongodb.DBObject; + +/** + * @author Mark Paluch + */ +class MapUtils { + /** + * Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a + * {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element + * collection for everything else. + * + * @param source + * @return + */ + static Collection asCollection(Object source) { + + if (source instanceof Collection) { + return (Collection) source; + } + + return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); + } + + @SuppressWarnings("unchecked") + static Map asMap(Bson bson) { + + if (bson instanceof Document) { + return (Document) bson; + } + + if (bson instanceof DBObject) { + return ((DBObject) bson).toMap(); + } + + throw new IllegalArgumentException( + String.format("Cannot read %s. as map. Given Bson must be a Document or DBObject!", bson.getClass())); + } + + static void addToMap(Bson bson, String key, @Nullable Object value) { + + if (bson instanceof Document) { + ((Document) bson).put(key, value); + return; + } + if (bson instanceof DBObject) { + ((DBObject) bson).put(key, value); + return; + } + throw new IllegalArgumentException(String.format( + "Cannot add key/value pair to %s. as map. Given Bson must be a Document or DBObject!", bson.getClass())); + } + + static void addAllToMap(Bson bson, Map value) { + + if (bson instanceof Document) { + ((Document) bson).putAll(value); + return; + } + + if (bson instanceof DBObject) { + ((DBObject) bson).putAll(value); + return; + } + + throw new IllegalArgumentException( + String.format("Cannot add all to %s. Given Bson must be a Document or DBObject.", bson.getClass())); + } + + static void removeFromMap(Bson bson, String key) { + + if (bson instanceof Document) { + ((Document) bson).remove(key); + return; + } + + if (bson instanceof DBObject) { + ((DBObject) bson).removeField(key); + return; + } + + throw new IllegalArgumentException( + String.format("Cannot remove from %s. Given Bson must be a Document or DBObject.", bson.getClass())); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index fd3a832e5..4cd799238 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -102,6 +102,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private static final String INCOMPATIBLE_TYPES = "Cannot convert %1$s of type %2$s into an instance of %3$s! Implement a custom Converter<%2$s, %3$s> and register it with the CustomConversions. Parent object was: %4$s"; private static final String INVALID_TYPE_TO_READ = "Expected to read Document %s into type %s but didn't find a PersistentEntity for the latter!"; + public static final ClassTypeInformation BSON = ClassTypeInformation.from(Bson.class); + protected static final Logger LOGGER = LoggerFactory.getLogger(MappingMongoConverter.class); protected final MappingContext, MongoPersistentProperty> mappingContext; @@ -137,9 +139,28 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App this::getWriteTarget); this.idMapper = new QueryMapper(this); + this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE); this.dbRefProxyHandler = new DefaultDbRefProxyHandler(spELContext, mappingContext, - MappingMongoConverter.this::getValueInternal); + (prop, bson, evaluator, path) -> { + + ConversionContext context = getConversionContext(path); + return MappingMongoConverter.this.getValueInternal(context, prop, bson, evaluator); + }); + } + + /** + * Creates a new {@link ConversionContext} given {@link ObjectPath}. + * + * @param path the current {@link ObjectPath}, must not be {@literal null}. + * @return the {@link ConversionContext}. + */ + protected ConversionContext getConversionContext(ObjectPath path) { + + Assert.notNull(path, "ObjectPath must not be null"); + + return new ConversionContext(path, this::readDocument, this::readCollectionOrArray, this::readMap, this::readDBRef, + this::getPotentiallyConvertedSimpleRead); } /** @@ -249,20 +270,20 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } protected S read(TypeInformation type, Bson bson) { - return read(type, bson, ObjectPath.ROOT); + return doRead(getConversionContext(ObjectPath.ROOT), type, bson); } - @Nullable @SuppressWarnings("unchecked") - private S read(TypeInformation type, Bson bson, ObjectPath path) { + private S doRead(ConversionContext context, TypeInformation type, Bson bson) { Assert.notNull(bson, "Bson must not be null!"); + // TODO: Cleanup duplication TypeInformation typeToUse = typeMapper.readType(bson, type); Class rawType = typeToUse.getType(); if (conversions.hasCustomReadTarget(bson.getClass(), rawType)) { - return conversionService.convert(bson, rawType); + return doConvert(bson, rawType); } if (Document.class.isAssignableFrom(rawType)) { @@ -282,26 +303,41 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return (S) bson; } - if (typeToUse.isCollectionLike() && bson instanceof List) { - return (S) readCollectionOrArray(typeToUse, (List) bson, path); + return context.convert(bson, typeToUse); + } + + /** + * Conversion method to materialize an object from a {@link Bson document}. Can be overridden by subclasses. + * + * @param context must not be {@literal null} + * @param bson must not be {@literal null} + * @param typeHint the {@link TypeInformation} to be used to unmarshall this {@link Document}. + * @return the converted object, will never be {@literal null}. + * @since 3.2 + */ + @SuppressWarnings("unchecked") + protected S readDocument(ConversionContext context, Bson bson, + TypeInformation typeHint) { + + // TODO: Cleanup duplication + + Document document = bson instanceof BasicDBObject ? new Document((BasicDBObject) bson) : (Document) bson; + TypeInformation typeToRead = typeMapper.readType(document, typeHint); + Class rawType = typeToRead.getType(); + + if (conversions.hasCustomReadTarget(bson.getClass(), rawType)) { + return doConvert(bson, rawType); } - if (typeToUse.isMap()) { - return (S) readMap(typeToUse, bson, path); - } - - if (bson instanceof Collection) { - throw new MappingException(String.format(INCOMPATIBLE_TYPES, bson, BasicDBList.class, typeToUse.getType(), path)); - } - - if (typeToUse.equals(ClassTypeInformation.OBJECT)) { + if (typeToRead.isMap()) { return (S) bson; } - // Retrieve persistent entity info - Document target = bson instanceof BasicDBObject ? new Document((BasicDBObject) bson) : (Document) bson; + if (BSON.isAssignableFrom(typeHint)) { + return (S) bson; + } - MongoPersistentEntity entity = mappingContext.getPersistentEntity(typeToUse); + MongoPersistentEntity entity = mappingContext.getPersistentEntity(typeToRead); if (entity == null) { @@ -309,29 +345,29 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Optional> codec = codecRegistryProvider.getCodecFor(rawType); if (codec.isPresent()) { - return codec.get().decode(new JsonReader(target.toJson()), DecoderContext.builder().build()); + return codec.get().decode(new JsonReader(document.toJson()), DecoderContext.builder().build()); } } - throw new MappingException(String.format(INVALID_TYPE_TO_READ, target, typeToUse.getType())); + throw new MappingException(String.format(INVALID_TYPE_TO_READ, document, rawType)); } - return read((MongoPersistentEntity) entity, target, path); + return read(context, (MongoPersistentEntity) entity, document); } - private ParameterValueProvider getParameterProvider(MongoPersistentEntity entity, - DocumentAccessor source, SpELExpressionEvaluator evaluator, ObjectPath path) { + private ParameterValueProvider getParameterProvider(ConversionContext context, + MongoPersistentEntity entity, DocumentAccessor source, SpELExpressionEvaluator evaluator) { - AssociationAwareMongoDbPropertyValueProvider provider = new AssociationAwareMongoDbPropertyValueProvider(source, - evaluator, path); + AssociationAwareMongoDbPropertyValueProvider provider = new AssociationAwareMongoDbPropertyValueProvider(context, + source, evaluator); PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( - entity, provider, path.getCurrentObject()); + entity, provider, context.getPath().getCurrentObject()); - return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider, - path); + return new ConverterAwareSpELExpressionParameterValueProvider(context, evaluator, conversionService, + parameterProvider); } - private S read(final MongoPersistentEntity entity, final Document bson, final ObjectPath path) { + private S read(ConversionContext context, MongoPersistentEntity entity, Document bson) { SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); DocumentAccessor documentAccessor = new DocumentAccessor(bson); @@ -339,20 +375,21 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); ParameterValueProvider provider = persistenceConstructor != null - && persistenceConstructor.hasParameters() ? getParameterProvider(entity, documentAccessor, evaluator, path) + && persistenceConstructor.hasParameters() ? getParameterProvider(context, entity, documentAccessor, evaluator) : NoOpParameterValueProvider.INSTANCE; EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); S instance = instantiator.createInstance(entity, provider); if (entity.requiresPropertyPopulation()) { - return populateProperties(entity, documentAccessor, path, evaluator, instance); + return populateProperties(context, entity, documentAccessor, evaluator, instance); } return instance; } - private S populateProperties(MongoPersistentEntity entity, DocumentAccessor documentAccessor, ObjectPath path, + private S populateProperties(ConversionContext context, MongoPersistentEntity entity, + DocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) { PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance), @@ -360,13 +397,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App // Make sure id property is set before all other properties - Object rawId = readAndPopulateIdentifier(accessor, documentAccessor, entity, path, evaluator); - ObjectPath currentPath = path.push(accessor.getBean(), entity, rawId); + Object rawId = readAndPopulateIdentifier(context, accessor, documentAccessor, entity, evaluator); + ObjectPath currentPath = context.getPath().push(accessor.getBean(), entity, rawId); + ConversionContext contextToUse = context.withPath(currentPath); - MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(documentAccessor, evaluator, - currentPath); + MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor, + evaluator); - readProperties(entity, accessor, documentAccessor, valueProvider, currentPath, evaluator); + readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator); return accessor.getBean(); } @@ -374,16 +412,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App /** * Reads the identifier from either the bean backing the {@link PersistentPropertyAccessor} or the source document in * case the identifier has not be populated yet. In this case the identifier is set on the bean for further reference. - * - * @param accessor must not be {@literal null}. - * @param document must not be {@literal null}. - * @param entity must not be {@literal null}. - * @param path - * @param evaluator - * @return */ - private Object readAndPopulateIdentifier(PersistentPropertyAccessor accessor, DocumentAccessor document, - MongoPersistentEntity entity, ObjectPath path, SpELExpressionEvaluator evaluator) { + @Nullable + private Object readAndPopulateIdentifier(ConversionContext context, PersistentPropertyAccessor accessor, + DocumentAccessor document, MongoPersistentEntity entity, SpELExpressionEvaluator evaluator) { Object rawId = document.getRawId(entity); @@ -397,22 +429,25 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return rawId; } - accessor.setProperty(idProperty, readIdValue(path, evaluator, idProperty, rawId)); + accessor.setProperty(idProperty, readIdValue(context, evaluator, idProperty, rawId)); return rawId; } - private Object readIdValue(ObjectPath path, SpELExpressionEvaluator evaluator, MongoPersistentProperty idProperty, + @Nullable + private Object readIdValue(ConversionContext context, SpELExpressionEvaluator evaluator, + MongoPersistentProperty idProperty, Object rawId) { String expression = idProperty.getSpelExpression(); Object resolvedValue = expression != null ? evaluator.evaluate(expression) : rawId; - return resolvedValue != null ? readValue(resolvedValue, idProperty.getTypeInformation(), path) : null; + return resolvedValue != null ? readValue(context, resolvedValue, idProperty.getTypeInformation()) : null; } - private void readProperties(MongoPersistentEntity entity, PersistentPropertyAccessor accessor, - DocumentAccessor documentAccessor, MongoDbPropertyValueProvider valueProvider, ObjectPath currentPath, + private void readProperties(ConversionContext context, MongoPersistentEntity entity, + PersistentPropertyAccessor accessor, DocumentAccessor documentAccessor, + MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator) { DbRefResolverCallback callback = null; @@ -422,7 +457,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (prop.isAssociation() && !entity.isConstructorArgument(prop)) { if (callback == null) { - callback = getDbRefResolverCallback(documentAccessor, currentPath, evaluator); + callback = getDbRefResolverCallback(context, documentAccessor, evaluator); } readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); @@ -432,7 +467,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (prop.isEmbedded()) { accessor.setProperty(prop, - readEmbedded(documentAccessor, currentPath, prop, mappingContext.getPersistentEntity(prop))); + readEmbedded(context, documentAccessor, prop, mappingContext.getRequiredPersistentEntity(prop))); continue; } @@ -449,7 +484,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (prop.isAssociation()) { if (callback == null) { - callback = getDbRefResolverCallback(documentAccessor, currentPath, evaluator); + callback = getDbRefResolverCallback(context, documentAccessor, evaluator); } readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback); @@ -460,11 +495,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } } - private DbRefResolverCallback getDbRefResolverCallback(DocumentAccessor documentAccessor, ObjectPath currentPath, + private DbRefResolverCallback getDbRefResolverCallback(ConversionContext context, DocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator) { - return new DefaultDbRefResolverCallback(documentAccessor.getDocument(), currentPath, evaluator, - MappingMongoConverter.this::getValueInternal); + return new DefaultDbRefResolverCallback(documentAccessor.getDocument(), context.getPath(), evaluator, + (prop, bson, e, path) -> MappingMongoConverter.this.getValueInternal(context, prop, bson, e)); } private void readAssociation(Association association, PersistentPropertyAccessor accessor, @@ -482,16 +517,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } @Nullable - private Object readEmbedded(DocumentAccessor documentAccessor, ObjectPath currentPath, MongoPersistentProperty prop, + private Object readEmbedded(ConversionContext context, DocumentAccessor documentAccessor, + MongoPersistentProperty prop, MongoPersistentEntity embeddedEntity) { if (prop.findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_EMPTY)) { - return read(embeddedEntity, (Document) documentAccessor.getDocument(), currentPath); + return read(context, embeddedEntity, (Document) documentAccessor.getDocument()); } for (MongoPersistentProperty persistentProperty : embeddedEntity) { if (documentAccessor.hasValue(persistentProperty)) { - return read(embeddedEntity, (Document) documentAccessor.getDocument(), currentPath); + return read(context, embeddedEntity, (Document) documentAccessor.getDocument()); } } return null; @@ -536,8 +572,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Object target = obj instanceof LazyLoadingProxy ? ((LazyLoadingProxy) obj).getTarget() : obj; writeInternal(target, bson, type); - if (asMap(bson).containsKey("_id") && asMap(bson).get("_id") == null) { - removeFromMap(bson, "_id"); + if (MapUtils.asMap(bson).containsKey("_id") && MapUtils.asMap(bson).get("_id") == null) { + MapUtils.removeFromMap(bson, "_id"); } if (requiresTypeHint(entityType)) { @@ -559,10 +595,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App /** * Internal write conversion method which should be used for nested invocations. - * - * @param obj - * @param bson - * @param typeHint */ @SuppressWarnings("unchecked") protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable TypeInformation typeHint) { @@ -575,8 +607,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Optional> customTarget = conversions.getCustomWriteTarget(entityType, Document.class); if (customTarget.isPresent()) { - Document result = conversionService.convert(obj, Document.class); - addAllToMap(bson, result); + Document result = doConvert(obj, Document.class); + MapUtils.addAllToMap(bson, result); return; } @@ -677,7 +709,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } if (valueType.isCollectionLike()) { - List collectionInternal = createCollection(asCollection(obj), prop); + List collectionInternal = createCollection(MapUtils.asCollection(obj), prop); accessor.put(prop, collectionInternal); return; } @@ -702,10 +734,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App dbRefObj = dbRefObj != null ? dbRefObj : createDBRef(obj, prop); - if (null != dbRefObj) { - accessor.put(prop, dbRefObj); - return; - } + accessor.put(prop, dbRefObj); + return; } /* @@ -720,7 +750,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (basicTargetType.isPresent()) { - accessor.put(prop, conversionService.convert(obj, basicTargetType.get())); + accessor.put(prop, doConvert(obj, basicTargetType.get())); return; } @@ -736,36 +766,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App accessor.put(prop, document); } - /** - * Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a - * {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element - * collection for everything else. - * - * @param source - * @return - */ - private static Collection asCollection(Object source) { - - if (source instanceof Collection) { - return (Collection) source; - } - - return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); - } - /** * Writes the given {@link Collection} using the given {@link MongoPersistentProperty} information. * * @param collection must not be {@literal null}. * @param property must not be {@literal null}. - * @return */ protected List createCollection(Collection collection, MongoPersistentProperty property) { if (!property.isDbReference()) { if (property.hasExplicitWriteTarget()) { - return writeCollectionInternal(collection, new TypeInformationWrapper<>(property), new ArrayList<>()); + return writeCollectionInternal(collection, new FieldTypeInformation<>(property), new ArrayList<>()); } return writeCollectionInternal(collection, property.getTypeInformation(), new BasicDBList()); } @@ -790,7 +802,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * * @param map must not {@literal null}. * @param property must not be {@literal null}. - * @return */ protected Bson createMap(Map map, MongoPersistentProperty property) { @@ -827,7 +838,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * @param source the collection to create a {@link Collection} for, must not be {@literal null}. * @param type the {@link TypeInformation} to consider or {@literal null} if unknown. * @param sink the {@link Collection} to write to. - * @return */ @SuppressWarnings("unchecked") private List writeCollectionInternal(Collection source, @Nullable TypeInformation type, @@ -849,7 +859,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App collection.add(getPotentiallyConvertedSimpleWrite(element, componentType != null ? componentType.getType() : Object.class)); } else if (element instanceof Collection || elementType.isArray()) { - collection.add(writeCollectionInternal(asCollection(element), componentType, new BasicDBList())); + collection.add(writeCollectionInternal(MapUtils.asCollection(element), componentType, new BasicDBList())); } else { Document document = new Document(); writeInternal(element, document, componentType); @@ -866,7 +876,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * @param obj must not be {@literal null}. * @param bson must not be {@literal null}. * @param propertyType must not be {@literal null}. - * @return */ protected Bson writeMapInternal(Map obj, Bson bson, TypeInformation propertyType) { @@ -881,14 +890,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (val == null || conversions.isSimpleType(val.getClass())) { writeSimpleInternal(val, bson, simpleKey); } else if (val instanceof Collection || val.getClass().isArray()) { - addToMap(bson, simpleKey, - writeCollectionInternal(asCollection(val), propertyType.getMapValueType(), new BasicDBList())); + MapUtils.addToMap(bson, simpleKey, + writeCollectionInternal(MapUtils.asCollection(val), propertyType.getMapValueType(), new BasicDBList())); } else { Document document = new Document(); TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType() : ClassTypeInformation.OBJECT; writeInternal(val, document, valueTypeInfo); - addToMap(bson, simpleKey, document); + MapUtils.addToMap(bson, simpleKey, document); } } else { throw new MappingException("Cannot use a complex object as a key value."); @@ -903,7 +912,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * conversions and escape dots from the result as they're not supported as {@link Map} key in MongoDB. * * @param key must not be {@literal null}. - * @return */ private String prepareMapKey(Object key) { @@ -918,8 +926,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * conversion if none is configured. * * @see #setMapKeyDotReplacement(String) - * @param source - * @return + * @param source must not be {@literal null}. */ protected String potentiallyEscapeMapKey(String source) { @@ -941,7 +948,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * Returns a {@link String} representation of the given {@link Map} key * * @param key - * @return */ private String potentiallyConvertMapKey(Object key) { @@ -958,8 +964,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * Translates the map key replacements in the given key just read with a dot in case a map key replacement has been * configured. * - * @param source - * @return + * @param source must not be {@literal null}. */ protected String potentiallyUnescapeMapKey(String source) { return mapKeyDotReplacement == null ? source : source.replaceAll(mapKeyDotReplacement, "\\."); @@ -969,13 +974,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * Adds custom type information to the given {@link Document} if necessary. That is if the value is not the same as * the one given. This is usually the case if you store a subtype of the actual declared type of the property. * - * @param type + * @param type can be {@literal null}. * @param value must not be {@literal null}. * @param bson must not be {@literal null}. */ protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation type, Object value, Bson bson) { - Class reference = type != null ? type.getActualType().getType() : Object.class; + Class reference = type != null ? type.getRequiredActualType().getType() : Object.class; Class valueType = ClassUtils.getUserClass(value.getClass()); boolean notTheSameClass = !valueType.equals(reference); @@ -987,15 +992,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App /** * Writes the given simple value to the given {@link Document}. Will store enum names for enum values. * - * @param value + * @param value can be {@literal null}. * @param bson must not be {@literal null}. * @param key must not be {@literal null}. */ - private void writeSimpleInternal(Object value, Bson bson, String key) { - addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class)); + private void writeSimpleInternal(@Nullable Object value, Bson bson, String key) { + MapUtils.addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class)); } - private void writeSimpleInternal(Object value, Bson bson, MongoPersistentProperty property) { + private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property) { DocumentAccessor accessor = new DocumentAccessor(bson); accessor.put(property, getPotentiallyConvertedSimpleWrite(value, property.hasExplicitWriteTarget() ? property.getFieldType() : Object.class)); @@ -1004,9 +1009,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App /** * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type. * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. - * - * @param value - * @return */ @Nullable private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class typeHint) { @@ -1018,14 +1020,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (typeHint != null && Object.class != typeHint) { if (conversionService.canConvert(value.getClass(), typeHint)) { - value = conversionService.convert(value, typeHint); + value = doConvert(value, typeHint); } } Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); if (customTarget.isPresent()) { - return conversionService.convert(value, customTarget.get()); + return doConvert(value, customTarget.get()); } if (ObjectUtils.isArray(value)) { @@ -1033,7 +1035,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (value instanceof byte[]) { return value; } - return asCollection(value); + return MapUtils.asCollection(value); } return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; @@ -1041,32 +1043,37 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App /** * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies - * {@link Enum} handling or returns the value as is. + * {@link Enum} handling or returns the value as is. Can be overridden by subclasses. * - * @param value - * @param target must not be {@literal null}. - * @return + * @since 3.2 */ - @Nullable - @SuppressWarnings({ "rawtypes", "unchecked" }) - private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { + protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation target) { + return getPotentiallyConvertedSimpleRead(value, target.getType()); + } - if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { + /** + * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies + * {@link Enum} handling or returns the value as is. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class target) { + + if (target == null || ClassUtils.isAssignableValue(target, value)) { return value; } if (conversions.hasCustomReadTarget(value.getClass(), target)) { - return conversionService.convert(value, target); + return doConvert(value, target); } if (Enum.class.isAssignableFrom(target)) { return Enum.valueOf((Class) target, value.toString()); } - return conversionService.convert(value, target); + return doConvert(value, target); } - protected DBRef createDBRef(Object target, MongoPersistentProperty property) { + protected DBRef createDBRef(Object target, @Nullable MongoPersistentProperty property) { Assert.notNull(target, "Target object must not be null!"); @@ -1102,24 +1109,26 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } @Nullable - private Object getValueInternal(MongoPersistentProperty prop, Bson bson, SpELExpressionEvaluator evaluator, - ObjectPath path) { - return new MongoDbPropertyValueProvider(bson, evaluator, path).getPropertyValue(prop); + private Object getValueInternal(ConversionContext context, MongoPersistentProperty prop, Bson bson, + SpELExpressionEvaluator evaluator) { + return new MongoDbPropertyValueProvider(context, bson, evaluator).getPropertyValue(prop); } /** - * Reads the given {@link BasicDBList} into a collection of the given {@link TypeInformation}. + * Reads the given {@link Collection} into a collection of the given {@link TypeInformation}. Can be overridden by + * subclasses. * - * @param targetType must not be {@literal null}. - * @param source must not be {@literal null}. - * @param path must not be {@literal null}. + * @param context must not be {@literal null} + * @param source must not be {@literal null} + * @param targetType the {@link Map} {@link TypeInformation} to be used to unmarshall this {@link Document}. + * @since 3.2 * @return the converted {@link Collection} or array, will never be {@literal null}. */ @SuppressWarnings("unchecked") - private Object readCollectionOrArray(TypeInformation targetType, Collection source, ObjectPath path) { + protected Object readCollectionOrArray(ConversionContext context, Collection source, + TypeInformation targetType) { Assert.notNull(targetType, "Target type must not be null!"); - Assert.notNull(path, "Object path must not be null!"); Class collectionType = targetType.isSubTypeOf(Collection.class) // ? targetType.getType() // @@ -1140,33 +1149,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (!DBRef.class.equals(rawComponentType) && isCollectionOfDbRefWhereBulkFetchIsPossible(source)) { - List objects = bulkReadAndConvertDBRefs((List) source, componentType, path, rawComponentType); + List objects = bulkReadAndConvertDBRefs(context, (List) source, componentType); return getPotentiallyConvertedSimpleRead(objects, targetType.getType()); } for (Object element : source) { - - if (element instanceof DBRef) { - items.add(DBRef.class.equals(rawComponentType) ? element - : readAndConvertDBRef((DBRef) element, componentType, path, rawComponentType)); - } else if (element instanceof Document) { - items.add(read(componentType, (Document) element, path)); - } else if (element instanceof BasicDBObject) { - items.add(read(componentType, (BasicDBObject) element, path)); - } else { - - if (!Object.class.equals(rawComponentType) && element instanceof Collection) { - if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) { - throw new MappingException( - String.format(INCOMPATIBLE_TYPES, element, element.getClass(), rawComponentType, path)); - } - } - if (element instanceof List) { - items.add(readCollectionOrArray(componentType, (Collection) element, path)); - } else { - items.add(getPotentiallyConvertedSimpleRead(element, rawComponentType)); - } - } + items.add(context.convert(element, componentType)); } return getPotentiallyConvertedSimpleRead(items, targetType.getType()); @@ -1179,26 +1167,42 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * @param bson must not be {@literal null} * @param path must not be {@literal null} * @return + * @deprecated since 3.2. Use {@link #readMap(ConversionContext, Bson, TypeInformation)} instead. */ - @SuppressWarnings("unchecked") + @Deprecated protected Map readMap(TypeInformation type, Bson bson, ObjectPath path) { + return readMap(getConversionContext(path), bson, type); + } + + /** + * Reads the given {@link Document} into a {@link Map}. will recursively resolve nested {@link Map}s as well. Can be + * overridden by subclasses. + * + * @param context must not be {@literal null} + * @param bson must not be {@literal null} + * @param targetType the {@link Map} {@link TypeInformation} to be used to unmarshall this {@link Document}. + * @return the converted {@link Map}, will never be {@literal null}. + * @since 3.2 + */ + protected Map readMap(ConversionContext context, Bson bson, TypeInformation targetType) { Assert.notNull(bson, "Document must not be null!"); - Assert.notNull(path, "Object path must not be null!"); + Assert.notNull(targetType, "TypeInformation must not be null!"); - Class mapType = typeMapper.readType(bson, type).getType(); + Class mapType = typeMapper.readType(bson, targetType).getType(); - TypeInformation keyType = type.getComponentType(); - TypeInformation valueType = type.getMapValueType(); + TypeInformation keyType = targetType.getComponentType(); + TypeInformation valueType = targetType.getMapValueType() == null ? ClassTypeInformation.OBJECT + : targetType.getRequiredMapValueType(); - Class rawKeyType = keyType != null ? keyType.getType() : null; - Class rawValueType = valueType != null ? valueType.getType() : null; + Class rawKeyType = keyType != null ? keyType.getType() : Object.class; + Class rawValueType = valueType.getType(); - Map sourceMap = asMap(bson); + Map sourceMap = MapUtils.asMap(bson); Map map = CollectionFactory.createMap(mapType, rawKeyType, sourceMap.keySet().size()); if (!DBRef.class.equals(rawValueType) && isCollectionOfDbRefWhereBulkFetchIsPossible(sourceMap.values())) { - bulkReadAndConvertDBRefMapIntoTarget(valueType, rawValueType, sourceMap, map); + bulkReadAndConvertDBRefMapIntoTarget(context, valueType, sourceMap, map); return map; } @@ -1210,92 +1214,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Object key = potentiallyUnescapeMapKey(entry.getKey()); - if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) { - key = conversionService.convert(key, rawKeyType); + if (!rawKeyType.isAssignableFrom(key.getClass())) { + key = doConvert(key, rawKeyType); } Object value = entry.getValue(); - TypeInformation defaultedValueType = valueType != null ? valueType : ClassTypeInformation.OBJECT; - - if (value instanceof Document) { - map.put(key, read(defaultedValueType, (Document) value, path)); - } else if (value instanceof BasicDBObject) { - map.put(key, read(defaultedValueType, (BasicDBObject) value, path)); - } else if (value instanceof DBRef) { - map.put(key, DBRef.class.equals(rawValueType) ? value - : readAndConvertDBRef((DBRef) value, defaultedValueType, ObjectPath.ROOT, rawValueType)); - } else if (value instanceof List) { - map.put(key, readCollectionOrArray(valueType != null ? valueType : ClassTypeInformation.LIST, - (List) value, path)); - } else { - map.put(key, getPotentiallyConvertedSimpleRead(value, rawValueType)); - } + map.put(key, context.convert(value, valueType)); } return map; } - @SuppressWarnings("unchecked") - private static Map asMap(Bson bson) { - - if (bson instanceof Document) { - return (Document) bson; - } - - if (bson instanceof DBObject) { - return ((DBObject) bson).toMap(); - } - - throw new IllegalArgumentException( - String.format("Cannot read %s. as map. Given Bson must be a Document or DBObject!", bson.getClass())); - } - - private static void addToMap(Bson bson, String key, @Nullable Object value) { - - if (bson instanceof Document) { - ((Document) bson).put(key, value); - return; - } - if (bson instanceof DBObject) { - ((DBObject) bson).put(key, value); - return; - } - throw new IllegalArgumentException(String.format( - "Cannot add key/value pair to %s. as map. Given Bson must be a Document or DBObject!", bson.getClass())); - } - - private static void addAllToMap(Bson bson, Map value) { - - if (bson instanceof Document) { - ((Document) bson).putAll(value); - return; - } - - if (bson instanceof DBObject) { - ((DBObject) bson).putAll(value); - return; - } - - throw new IllegalArgumentException( - String.format("Cannot add all to %s. Given Bson must be a Document or DBObject.", bson.getClass())); - } - - private static void removeFromMap(Bson bson, String key) { - - if (bson instanceof Document) { - ((Document) bson).remove(key); - return; - } - - if (bson instanceof DBObject) { - ((DBObject) bson).removeField(key); - return; - } - - throw new IllegalArgumentException( - String.format("Cannot remove from %s. Given Bson must be a Document or DBObject.", bson.getClass())); - } - /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.convert.MongoWriter#convertToMongoType(java.lang.Object, org.springframework.data.util.TypeInformation) @@ -1303,7 +1232,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @Nullable @SuppressWarnings("unchecked") @Override - public Object convertToMongoType(@Nullable Object obj, TypeInformation typeInformation) { + public Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation) { if (obj == null) { return null; @@ -1311,7 +1240,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Optional> target = conversions.getCustomWriteTarget(obj.getClass()); if (target.isPresent()) { - return conversionService.convert(obj, target.get()); + return doConvert(obj, target.get()); } if (conversions.isSimpleType(obj.getClass())) { @@ -1386,7 +1315,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return !obj.getClass().equals(typeInformation.getType()) ? newDocument : removeTypeInfo(newDocument, true); } - @Nullable @Override public Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { Document newDocument = new Document(); @@ -1394,7 +1322,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return newDocument; } - public List maybeConvertList(Iterable source, TypeInformation typeInformation) { + // TODO: hide + public List maybeConvertList(Iterable source, @Nullable TypeInformation typeInformation) { List newDbl = new ArrayList<>(); @@ -1458,203 +1387,52 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return document; } - /** - * {@link PropertyValueProvider} to evaluate a SpEL expression if present on the property or simply accesses the field - * of the configured source {@link Document}. - * - * @author Oliver Gierke - * @author Mark Paluch - * @author Christoph Strobl - */ - class MongoDbPropertyValueProvider implements PropertyValueProvider { - - final DocumentAccessor accessor; - final SpELExpressionEvaluator evaluator; - final ObjectPath path; - - /** - * Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and - * {@link ObjectPath}. - * - * @param source must not be {@literal null}. - * @param evaluator must not be {@literal null}. - * @param path must not be {@literal null}. - */ - MongoDbPropertyValueProvider(Bson source, SpELExpressionEvaluator evaluator, ObjectPath path) { - this(new DocumentAccessor(source), evaluator, path); - } - - /** - * Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and - * {@link ObjectPath}. - * - * @param accessor must not be {@literal null}. - * @param evaluator must not be {@literal null}. - * @param path must not be {@literal null}. - */ - MongoDbPropertyValueProvider(DocumentAccessor accessor, SpELExpressionEvaluator evaluator, ObjectPath path) { - - Assert.notNull(accessor, "DocumentAccessor must no be null!"); - Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null!"); - Assert.notNull(path, "ObjectPath must not be null!"); - - this.accessor = accessor; - this.evaluator = evaluator; - this.path = path; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.convert.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) - */ - @Nullable - public T getPropertyValue(MongoPersistentProperty property) { - - String expression = property.getSpelExpression(); - Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property); - - if (value == null) { - return null; - } - - return readValue(value, property.getTypeInformation(), path); - } - } - - /** - * {@link PropertyValueProvider} that is aware of {@link MongoPersistentProperty#isAssociation()} and that delegates - * resolution to {@link DbRefResolver}. - * - * @author Mark Paluch - * @author Christoph Strobl - * @since 2.1 - */ - class AssociationAwareMongoDbPropertyValueProvider extends MongoDbPropertyValueProvider { - - /** - * Creates a new {@link AssociationAwareMongoDbPropertyValueProvider} for the given source, - * {@link SpELExpressionEvaluator} and {@link ObjectPath}. - * - * @param source must not be {@literal null}. - * @param evaluator must not be {@literal null}. - * @param path must not be {@literal null}. - */ - AssociationAwareMongoDbPropertyValueProvider(DocumentAccessor source, SpELExpressionEvaluator evaluator, - ObjectPath path) { - super(source, evaluator, path); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.convert.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) - */ - @Nullable - @SuppressWarnings("unchecked") - public T getPropertyValue(MongoPersistentProperty property) { - - if (property.isDbReference() && property.getDBRef().lazy()) { - - Object rawRefValue = accessor.get(property); - if (rawRefValue == null) { - return null; - } - - DbRefResolverCallback callback = new DefaultDbRefResolverCallback(accessor.getDocument(), path, evaluator, - MappingMongoConverter.this::getValueInternal); - - DBRef dbref = rawRefValue instanceof DBRef ? (DBRef) rawRefValue : null; - return (T) dbRefResolver.resolveDbRef(property, dbref, callback, dbRefProxyHandler); - } - - return super.getPropertyValue(property); - } - } - - /** - * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw - * resolved SpEL value. - * - * @author Oliver Gierke - */ - private class ConverterAwareSpELExpressionParameterValueProvider - extends SpELExpressionParameterValueProvider { - - private final ObjectPath path; - - /** - * Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}. - * - * @param evaluator must not be {@literal null}. - * @param conversionService must not be {@literal null}. - * @param delegate must not be {@literal null}. - */ - public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, - ConversionService conversionService, ParameterValueProvider delegate, - ObjectPath path) { - - super(evaluator, conversionService, delegate); - this.path = path; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.model.SpELExpressionParameterValueProvider#potentiallyConvertSpelValue(java.lang.Object, org.springframework.data.mapping.PreferredConstructor.Parameter) - */ - @Override - protected T potentiallyConvertSpelValue(Object object, Parameter parameter) { - return readValue(object, parameter.getType(), path); - } - } - @Nullable @SuppressWarnings("unchecked") - T readValue(Object value, TypeInformation type, ObjectPath path) { + T readValue(ConversionContext context, @Nullable Object value, TypeInformation type) { + + if (value == null) { + return null; + } + + Assert.notNull(type, "TypeInformation must not be null"); Class rawType = type.getType(); if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { - return (T) conversionService.convert(value, rawType); + return (T) doConvert(value, rawType); } else if (value instanceof DBRef) { - return potentiallyReadOrResolveDbRef((DBRef) value, type, path, rawType); - } else if (value instanceof List) { - return (T) readCollectionOrArray(type, (List) value, path); - } else if (value instanceof Document) { - return (T) read(type, (Document) value, path); - } else if (value instanceof DBObject) { - return (T) read(type, (BasicDBObject) value, path); - } else { - return (T) getPotentiallyConvertedSimpleRead(value, rawType); + return (T) readDBRef(context, (DBRef) value, type); } + + return (T) context.convert(value, type); } @Nullable - @SuppressWarnings("unchecked") - private T potentiallyReadOrResolveDbRef(@Nullable DBRef dbref, TypeInformation type, ObjectPath path, - Class rawType) { + private Object readDBRef(ConversionContext context, @Nullable DBRef dbref, TypeInformation type) { - if (rawType.equals(DBRef.class)) { - return (T) dbref; + if (type.getType().equals(DBRef.class)) { + return dbref; } - T object = dbref == null ? null : path.getPathItem(dbref.getId(), dbref.getCollectionName(), (Class) rawType); - return object != null ? object : readAndConvertDBRef(dbref, type, path, rawType); - } + ObjectPath path = context.getPath(); - @Nullable - private T readAndConvertDBRef(@Nullable DBRef dbref, TypeInformation type, ObjectPath path, - @Nullable Class rawType) { + Object object = dbref == null ? null : path.getPathItem(dbref.getId(), dbref.getCollectionName(), type.getType()); + if (object != null) { + return object; + } - List result = bulkReadAndConvertDBRefs(Collections.singletonList(dbref), type, path, rawType); + List result = bulkReadAndConvertDBRefs(context, Collections.singletonList(dbref), type); return CollectionUtils.isEmpty(result) ? null : result.iterator().next(); } @SuppressWarnings({ "unchecked", "rawtypes" }) - private void bulkReadAndConvertDBRefMapIntoTarget(TypeInformation valueType, Class rawValueType, + private void bulkReadAndConvertDBRefMapIntoTarget(ConversionContext context, TypeInformation valueType, Map sourceMap, Map targetMap) { LinkedHashMap referenceMap = new LinkedHashMap<>(sourceMap); - List convertedObjects = bulkReadAndConvertDBRefs((List) new ArrayList(referenceMap.values()), - valueType, ObjectPath.ROOT, rawValueType); + List convertedObjects = bulkReadAndConvertDBRefs(context.withPath(ObjectPath.ROOT), + (List) new ArrayList(referenceMap.values()), valueType); int index = 0; for (String key : referenceMap.keySet()) { @@ -1664,8 +1442,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } @SuppressWarnings("unchecked") - private List bulkReadAndConvertDBRefs(List dbrefs, TypeInformation type, ObjectPath path, - @Nullable Class rawType) { + private List bulkReadAndConvertDBRefs(ConversionContext context, List dbrefs, TypeInformation type) { if (CollectionUtils.isEmpty(dbrefs)) { return Collections.emptyList(); @@ -1684,8 +1461,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (document != null) { maybeEmitEvent( - new AfterLoadEvent<>(document, (Class) (rawType != null ? rawType : Object.class), collectionName)); - target = (T) read(type, document, path); + new AfterLoadEvent<>(document, (Class) type.getType(), collectionName)); + target = (T) doRead(context, type, document); } if (target != null) { @@ -1772,6 +1549,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return target; } + @SuppressWarnings("ConstantConditions") + private T doConvert(Object value, Class target) { + return conversionService.convert(value, target); + } + /** * Returns whether the given {@link Iterable} contains {@link DBRef} instances all pointing to the same collection. * @@ -1800,6 +1582,160 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return true; } + /** + * {@link PropertyValueProvider} to evaluate a SpEL expression if present on the property or simply accesses the field + * of the configured source {@link Document}. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Christoph Strobl + */ + static class MongoDbPropertyValueProvider implements PropertyValueProvider { + + final ConversionContext context; + final DocumentAccessor accessor; + final SpELExpressionEvaluator evaluator; + + /** + * Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and + * {@link ObjectPath}. + * + * @param context must not be {@literal null}. + * @param source must not be {@literal null}. + * @param evaluator must not be {@literal null}. + */ + MongoDbPropertyValueProvider(ConversionContext context, Bson source, SpELExpressionEvaluator evaluator) { + this(context, new DocumentAccessor(source), evaluator); + } + + /** + * Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and + * {@link ObjectPath}. + * + * @param context must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param evaluator must not be {@literal null}. + */ + MongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor accessor, + SpELExpressionEvaluator evaluator) { + + Assert.notNull(context, "ConversionContext must no be null!"); + Assert.notNull(accessor, "DocumentAccessor must no be null!"); + Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null!"); + + this.context = context; + this.accessor = accessor; + this.evaluator = evaluator; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) + */ + @Nullable + @SuppressWarnings("unchecked") + public T getPropertyValue(MongoPersistentProperty property) { + + String expression = property.getSpelExpression(); + Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property); + + if (value == null) { + return null; + } + + return (T) context.convert(value, property.getTypeInformation()); + } + } + + /** + * {@link PropertyValueProvider} that is aware of {@link MongoPersistentProperty#isAssociation()} and that delegates + * resolution to {@link DbRefResolver}. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ + class AssociationAwareMongoDbPropertyValueProvider extends MongoDbPropertyValueProvider { + + /** + * Creates a new {@link AssociationAwareMongoDbPropertyValueProvider} for the given source, + * {@link SpELExpressionEvaluator} and {@link ObjectPath}. + * + * @param source must not be {@literal null}. + * @param evaluator must not be {@literal null}. + */ + AssociationAwareMongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor source, + SpELExpressionEvaluator evaluator) { + super(context, source, evaluator); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) + */ + @Nullable + @SuppressWarnings("unchecked") + public T getPropertyValue(MongoPersistentProperty property) { + + if (property.isDbReference() && property.getDBRef().lazy()) { + + Object rawRefValue = accessor.get(property); + if (rawRefValue == null) { + return null; + } + + DbRefResolverCallback callback = new DefaultDbRefResolverCallback(accessor.getDocument(), context.getPath(), + evaluator, (prop, bson, evaluator, path) -> MappingMongoConverter.this.getValueInternal(context, prop, bson, + evaluator)); + + DBRef dbref = rawRefValue instanceof DBRef ? (DBRef) rawRefValue : null; + return (T) dbRefResolver.resolveDbRef(property, dbref, callback, dbRefProxyHandler); + } + + return super.getPropertyValue(property); + } + } + + /** + * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw + * resolved SpEL value. + * + * @author Oliver Gierke + */ + private static class ConverterAwareSpELExpressionParameterValueProvider + extends SpELExpressionParameterValueProvider { + + private final ConversionContext context; + + /** + * Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}. + * + * @param context must not be {@literal null}. + * @param evaluator must not be {@literal null}. + * @param conversionService must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public ConverterAwareSpELExpressionParameterValueProvider(ConversionContext context, + SpELExpressionEvaluator evaluator, ConversionService conversionService, + ParameterValueProvider delegate) { + + super(evaluator, conversionService, delegate); + + Assert.notNull(context, "ConversionContext must no be null!"); + + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.SpELExpressionParameterValueProvider#potentiallyConvertSpelValue(java.lang.Object, org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + protected T potentiallyConvertSpelValue(Object object, Parameter parameter) { + return context.convert(object, parameter.getType()); + } + } + /** * Marker class used to indicate we have a non root document object here that might be used within an update - so we * need to preserve type hints for potential nested elements but need to remove it on top level. @@ -1821,15 +1757,21 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } } - private static class TypeInformationWrapper implements TypeInformation { + /** + * {@link TypeInformation} considering {@link MongoPersistentProperty#getFieldType()} as type source. + * + * @param + */ + private static class FieldTypeInformation implements TypeInformation { - private MongoPersistentProperty persistentProperty; - private TypeInformation delegate; + private final MongoPersistentProperty persistentProperty; + private final TypeInformation delegate; - public TypeInformationWrapper(MongoPersistentProperty property) { + @SuppressWarnings("unchecked") + public FieldTypeInformation(MongoPersistentProperty property) { this.persistentProperty = property; - this.delegate = property.getTypeInformation(); + this.delegate = (TypeInformation) property.getTypeInformation(); } @Override @@ -1863,7 +1805,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } @Override - public Class getType() { + public Class getType() { return delegate.getType(); } @@ -1903,8 +1845,125 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } @Override - public org.springframework.data.util.TypeInformation specialize(ClassTypeInformation type) { + public org.springframework.data.util.TypeInformation specialize(ClassTypeInformation type) { return delegate.specialize(type); } } + + /** + * Conversion context holding references to simple {@link ValueConverter} and {@link ContainerValueConverter}. + * Entrypoint for recursive conversion of {@link Document} and other types. + * + * @since 3.2 + */ + protected static class ConversionContext { + + private final ObjectPath path; + private final ContainerValueConverter documentConverter; + private final ContainerValueConverter> collectionConverter; + private final ContainerValueConverter mapConverter; + private final ContainerValueConverter dbRefConverter; + private final ValueConverter elementConverter; + + ConversionContext(ObjectPath path, ContainerValueConverter documentConverter, + ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, + ContainerValueConverter dbRefConverter, ValueConverter elementConverter) { + + this.path = path; + this.documentConverter = documentConverter; + this.collectionConverter = collectionConverter; + this.mapConverter = mapConverter; + this.dbRefConverter = dbRefConverter; + this.elementConverter = elementConverter; + } + + /** + * Converts a source object into {@link TypeInformation target}. + * + * @param source must not be {@literal null}. + * @param typeHint must not be {@literal null}. + * @return the converted object. + */ + @SuppressWarnings("unchecked") + public S convert(Object source, TypeInformation typeHint) { + + Assert.notNull(typeHint, "TypeInformation must not be null"); + + if (source instanceof Collection) { + + Class rawType = typeHint.getType(); + if (!Object.class.equals(rawType)) { + if (!rawType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawType)) { + throw new MappingException( + String.format(INCOMPATIBLE_TYPES, source, source.getClass(), rawType, getPath())); + } + } + + if (typeHint.isCollectionLike() || typeHint.getType().isAssignableFrom(Collection.class)) { + return (S) collectionConverter.convert(this, (Collection) source, typeHint); + } + } + + if (typeHint.isMap()) { + return (S) mapConverter.convert(this, (Bson) source, typeHint); + } + + if (source instanceof DBRef) { + return (S) dbRefConverter.convert(this, (DBRef) source, typeHint); + } + + if (source instanceof Collection) { + throw new MappingException( + String.format(INCOMPATIBLE_TYPES, source, BasicDBList.class, typeHint.getType(), getPath())); + } + + if (source instanceof Bson) { + return (S) documentConverter.convert(this, (Bson) source, typeHint); + } + + return (S) elementConverter.convert(source, typeHint); + } + + /** + * Create a new {@link ConversionContext} with {@link ObjectPath currentPath} applied. + * + * @param currentPath must not be {@literal null}. + * @return a new {@link ConversionContext} with {@link ObjectPath currentPath} applied. + */ + public ConversionContext withPath(ObjectPath currentPath) { + + Assert.notNull(currentPath, "ObjectPath must not be null"); + + return new ConversionContext(currentPath, documentConverter, collectionConverter, mapConverter, dbRefConverter, + elementConverter); + } + + public ObjectPath getPath() { + return path; + } + + /** + * Converts a simple {@code source} value into {@link TypeInformation the target type}. + * + * @param + */ + interface ValueConverter { + + Object convert(T source, TypeInformation typeHint); + + } + + /** + * Converts a container {@code source} value into {@link TypeInformation the target type}. Containers may + * recursively apply conversions for entities, collections, maps, etc. + * + * @param + */ + interface ContainerValueConverter { + + Object convert(ConversionContext context, T source, TypeInformation typeHint); + + } + + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java index a4c1ab788..2c0f8649e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java @@ -62,7 +62,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; /** - * Unit tests for {@link DbRefMappingMongoConverter}. + * Unit tests for {@link MappingMongoConverter}. * * @author Oliver Gierke * @author Thomas Darimont diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index abdd84a4a..ffad28b23 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -2177,9 +2177,10 @@ class MappingMongoConverterUnitTests { MappingMongoConverter spyConverter = spy(converter); Mockito.doReturn(cluster).when(spyConverter).readRef(dbRef); - Map result = spyConverter.readMap(ClassTypeInformation.MAP, data, ObjectPath.ROOT); + Map result = spyConverter.readMap(spyConverter.getConversionContext(ObjectPath.ROOT), data, + ClassTypeInformation.MAP); - assertThat(((LinkedHashMap) result.get("cluster")).get("_id")).isEqualTo(100L); + assertThat(((Map) result.get("cluster")).get("_id")).isEqualTo(100L); } @Test // GH-3546