diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 3bba17aae..b5af06758 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -23,13 +23,17 @@ import java.util.Optional; import org.bson.Document; import org.springframework.core.convert.ConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.EntityProjection; +import org.springframework.data.mapping.context.EntityProjectionIntrospector; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions; +import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoWriter; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -39,6 +43,7 @@ import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.timeseries.Granularity; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -63,8 +68,19 @@ class EntityOperations { private final MappingContext, MongoPersistentProperty> context; - EntityOperations(MappingContext, MongoPersistentProperty> context) { + private final EntityProjectionIntrospector introspector; + + EntityOperations(MongoConverter converter) { + this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory()); + } + + EntityOperations(MappingContext, MongoPersistentProperty> context, + CustomConversions conversions, ProjectionFactory projectionFactory) { this.context = context; + this.introspector = EntityProjectionIntrospector.create(projectionFactory, + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and(((target, underlyingType) -> !conversions.isSimpleType(target))), + context); } /** @@ -229,6 +245,11 @@ class EntityOperations { return UntypedOperations.instance(); } + public EntityProjection introspectProjection(Class resultType, + Class entityType) { + return introspector.introspect(resultType, entityType); + } + /** * A representation of information about an entity. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 895097dd4..15b6b5b79 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -49,6 +49,7 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; @@ -102,7 +103,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; @@ -173,7 +173,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final QueryMapper queryMapper; private final UpdateMapper updateMapper; private final JsonSchemaMapper schemaMapper; - private final SpelAwareProxyProjectionFactory projectionFactory; private final EntityOperations operations; private final PropertyOperations propertyOperations; private final QueryOperations queryOperations; @@ -225,8 +224,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.queryMapper = new QueryMapper(this.mongoConverter); this.updateMapper = new UpdateMapper(this.mongoConverter); this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter); - this.projectionFactory = new SpelAwareProxyProjectionFactory(); - this.operations = new EntityOperations(this.mongoConverter.getMappingContext()); + this.operations = new EntityOperations(this.mongoConverter); this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations, mongoDbFactory); @@ -264,7 +262,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.queryMapper = that.queryMapper; this.updateMapper = that.updateMapper; this.schemaMapper = that.schemaMapper; - this.projectionFactory = that.projectionFactory; this.mappingContext = that.mappingContext; this.operations = that.operations; this.propertyOperations = that.propertyOperations; @@ -330,9 +327,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } resourceLoader = applicationContext; - - projectionFactory.setBeanFactory(applicationContext); - projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); } /** @@ -416,15 +410,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); + EntityProjection projection = operations.introspectProjection(returnType, + entityType); Document mappedQuery = queryContext.getMappedQuery(persistentEntity); - Document mappedFields = queryContext.getMappedFields(persistentEntity, returnType, projectionFactory); + Document mappedFields = queryContext.getMappedFields(persistentEntity, projection); FindIterable cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection, col -> col.find(mappedQuery, Document.class).projection(mappedFields)); return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, - new ProjectingReadCallback<>(mongoConverter, entityType, returnType, collectionName)); + new ProjectingReadCallback<>(mongoConverter, projection, collectionName)); }); } @@ -964,9 +960,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, .withOptions(AggregationOptions.builder().collation(near.getCollation()).build()); AggregationResults results = aggregate($geoNear, collection, Document.class); + EntityProjection projection = operations.introspectProjection(returnType, + domainType); DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, - new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collection), near.getMetric()); + new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric()); List> result = new ArrayList<>(); @@ -1050,8 +1048,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); + EntityProjection projection = operations.introspectProjection(resultType, + entityType); Document mappedQuery = queryContext.getMappedQuery(entity); - Document mappedFields = queryContext.getMappedFields(entity, resultType, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, projection); Document mappedSort = queryContext.getMappedSort(entity); replacement = maybeCallBeforeConvert(replacement, collectionName); @@ -1061,7 +1061,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, maybeCallBeforeSave(replacement, mappedReplacement, collectionName); T saved = doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, - queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, resultType); + queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, + projection); if (saved != null) { maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName)); @@ -2499,7 +2500,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); - Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2551,7 +2553,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); - Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2573,9 +2576,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Class targetClass, CursorPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); + EntityProjection projection = operations.introspectProjection(targetClass, + sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); - Document mappedFields = queryContext.getMappedFields(entity, targetClass, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, projection); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2584,9 +2589,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields, null), preparer, - new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName); + new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); } + /** * Convert given {@link CollectionOptions} to a document and take the domain type information into account when * creating a mapped schema for validation.
@@ -2745,6 +2751,35 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { + EntityProjection projection = operations.introspectProjection(resultType, + entityType); + + return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, + options, projection); + } + + /** + * Customize this part for findAndReplace. + * + * @param collectionName The name of the collection to perform the operation in. + * @param mappedQuery the query to look up documents. + * @param mappedFields the fields to project the result to. + * @param mappedSort the sort to be applied when executing the query. + * @param collation collation settings for the query. Can be {@literal null}. + * @param entityType the source domain type. + * @param replacement the replacement {@link Document}. + * @param options applicable options. + * @param projection the projection descriptor. + * @return {@literal null} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is + * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. + * @since 3.4 + */ + @Nullable + private T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, Class entityType, + Document replacement, FindAndReplaceOptions options, + EntityProjection projection) { + if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format( "findAndReplace using query: %s fields: %s sort: %s for class: %s and replacement: %s " + "in collection: %s", @@ -2754,7 +2789,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return executeFindOneInternal( new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(mongoConverter, entityType, resultType, collectionName), collectionName); + new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); } /** @@ -3205,17 +3240,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ private class ProjectingReadCallback implements DocumentCallback { - private final EntityReader reader; - private final Class entityType; - private final Class targetType; + private final MongoConverter reader; + private final EntityProjection projection; private final String collectionName; - ProjectingReadCallback(EntityReader reader, Class entityType, Class targetType, + ProjectingReadCallback(MongoConverter reader, EntityProjection projection, String collectionName) { this.reader = reader; - this.entityType = entityType; - this.targetType = targetType; + this.projection = projection; this.collectionName = collectionName; } @@ -3230,21 +3263,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return null; } - Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType - : targetType; + maybeEmitEvent(new AfterLoadEvent<>(document, projection.getMappedType().getType(), collectionName)); - maybeEmitEvent(new AfterLoadEvent<>(document, targetType, collectionName)); - - Object entity = reader.read(typeToRead, document); + Object entity = reader.project(projection, document); if (entity == null) { throw new MappingException(String.format("EntityReader %s returned null", reader)); } - Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity; - - maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName)); - return (T) maybeCallAfterConvert(result, document, collectionName); + maybeEmitEvent(new AfterConvertEvent<>(document, entity, collectionName)); + return (T) maybeCallAfterConvert(entity, document, collectionName); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java index 5eb9f110b..eddad2ba4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/PropertyOperations.java @@ -16,18 +16,19 @@ package org.springframework.data.mongodb.core; import org.bson.Document; -import org.springframework.data.mapping.SimplePropertyHandler; + +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.projection.ProjectionInformation; -import org.springframework.util.ClassUtils; +import org.springframework.data.mongodb.core.mapping.PersistentPropertyTranslator; +import org.springframework.data.util.Predicates; /** * Common operations performed on properties of an entity like extracting fields information for projection creation. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 */ class PropertyOperations { @@ -40,37 +41,37 @@ class PropertyOperations { /** * For cases where {@code fields} is {@link Document#isEmpty() empty} include only fields that are required for - * creating the projection (target) type if the {@code targetType} is a {@literal DTO projection} or a + * creating the projection (target) type if the {@code EntityProjection} is a {@literal DTO projection} or a * {@literal closed interface projection}. * - * @param projectionFactory must not be {@literal null}. + * @param projection must not be {@literal null}. * @param fields must not be {@literal null}. - * @param domainType must not be {@literal null}. - * @param targetType must not be {@literal null}. * @return {@link Document} with fields to be included. */ - Document computeFieldsForProjection(ProjectionFactory projectionFactory, Document fields, Class domainType, - Class targetType) { + Document computeMappedFieldsForProjection(EntityProjection projection, + Document fields) { - if (!fields.isEmpty() || ClassUtils.isAssignable(domainType, targetType)) { + if (!projection.isProjection() || !projection.isClosedProjection()) { return fields; } Document projectedFields = new Document(); - if (targetType.isInterface()) { - - ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(targetType); - - if (projectionInformation.isClosed()) { - projectionInformation.getInputProperties().forEach(it -> projectedFields.append(it.getName(), 1)); - } + if (projection.getMappedType().getType().isInterface()) { + projection.forEach(it -> { + projectedFields.put(it.getPropertyPath().getSegment(), 1); + }); } else { - MongoPersistentEntity entity = mappingContext.getPersistentEntity(targetType); - if (entity != null) { - entity.doWithProperties( - (SimplePropertyHandler) persistentProperty -> projectedFields.append(persistentProperty.getName(), 1)); + // DTO projections use merged metadata between domain type and result type + PersistentPropertyTranslator translator = PersistentPropertyTranslator.create( + mappingContext.getRequiredPersistentEntity(projection.getDomainType()), + Predicates.negate(MongoPersistentProperty::hasExplicitFieldName)); + + MongoPersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(projection.getMappedType()); + for (MongoPersistentProperty property : persistentEntity) { + projectedFields.put(translator.translate(property).getFieldName(), 1); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index e9431aa3d..5bf6226bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -28,8 +28,10 @@ import java.util.stream.Collectors; import org.bson.BsonValue; import org.bson.Document; import org.bson.codecs.Codec; + import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.MongoExpression; @@ -54,11 +56,9 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import com.mongodb.client.model.CountOptions; @@ -288,45 +288,58 @@ class QueryOperations { return queryMapper.getMappedObject(getQueryObject(), entity); } - Document getMappedFields(@Nullable MongoPersistentEntity entity, Class targetType, - ProjectionFactory projectionFactory) { + Document getMappedFields(@Nullable MongoPersistentEntity entity, + EntityProjection projection) { - Document fields = new Document(); + Document fields = evaluateFields(entity); - for (Entry entry : query.getFieldsObject().entrySet()) { + if (entity == null) { + return fields; + } - if (entry.getValue() instanceof MongoExpression) { + Document mappedFields; + if (!fields.isEmpty()) { + mappedFields = queryMapper.getMappedFields(fields, entity); + } else { + mappedFields = propertyOperations.computeMappedFieldsForProjection(projection, fields); + } - AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT - : new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper); + if (entity.hasTextScoreProperty() && mappedFields.containsKey(entity.getTextScoreProperty().getFieldName()) + && !query.getQueryObject().containsKey("$text")) { + mappedFields.remove(entity.getTextScoreProperty().getFieldName()); + } - fields.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx)); - } else { - fields.put(entry.getKey(), entry.getValue()); - } + if (mappedFields.isEmpty()) { + return BsonUtils.EMPTY_DOCUMENT; } - Document mappedFields = fields; + return mappedFields; + } + + private Document evaluateFields(@Nullable MongoPersistentEntity entity) { - if (entity == null) { - return mappedFields; + Document fields = query.getFieldsObject(); + + if (fields.isEmpty()) { + return BsonUtils.EMPTY_DOCUMENT; } - Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields, - entity.getType(), targetType); + Document evaluated = new Document(); - if (ObjectUtils.nullSafeEquals(fields, projectedFields)) { - mappedFields = queryMapper.getMappedFields(projectedFields, entity); - } else { - mappedFields = queryMapper.getMappedFields(projectedFields, - mappingContext.getRequiredPersistentEntity(targetType)); - } + for (Entry entry : fields.entrySet()) { - if (entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) { - mappedFields.remove(entity.getTextScoreProperty().getFieldName()); + if (entry.getValue() instanceof MongoExpression) { + + AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT + : new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper); + + evaluated.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx)); + } else { + evaluated.put(entry.getKey(), entry.getValue()); + } } - return mappedFields; + return evaluated; } /** @@ -388,8 +401,8 @@ class QueryOperations { } @Override - Document getMappedFields(@Nullable MongoPersistentEntity entity, Class targetType, - ProjectionFactory projectionFactory) { + Document getMappedFields(@Nullable MongoPersistentEntity entity, + EntityProjection projection) { return getMappedFields(entity); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index e408e62cd..3cadc2c1b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -63,6 +63,7 @@ import org.springframework.data.geo.Metric; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDatabaseFactory; @@ -113,7 +114,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -175,7 +175,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final QueryMapper queryMapper; private final UpdateMapper updateMapper; private final JsonSchemaMapper schemaMapper; - private final SpelAwareProxyProjectionFactory projectionFactory; private final ApplicationListener> indexCreatorListener; private final EntityOperations operations; private final PropertyOperations propertyOperations; @@ -242,13 +241,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.queryMapper = new QueryMapper(this.mongoConverter); this.updateMapper = new UpdateMapper(this.mongoConverter); this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter); - this.projectionFactory = new SpelAwareProxyProjectionFactory(); this.indexCreatorListener = new IndexCreatorEventListener(subscriptionExceptionHandler); // We always have a mapping context in the converter, whether it's a simple one or not this.mappingContext = this.mongoConverter.getMappingContext(); - this.operations = new EntityOperations(this.mappingContext); - this.propertyOperations = new PropertyOperations(this.mappingContext); + this.operations = new EntityOperations(this.mongoConverter); + this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext()); this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations, mongoDatabaseFactory); @@ -276,7 +274,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.queryMapper = that.queryMapper; this.updateMapper = that.updateMapper; this.schemaMapper = that.schemaMapper; - this.projectionFactory = that.projectionFactory; this.indexCreator = that.indexCreator; this.indexCreatorListener = that.indexCreatorListener; this.mappingContext = that.mappingContext; @@ -353,9 +350,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } - - projectionFactory.setBeanFactory(applicationContext); - projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); } /** @@ -1058,9 +1052,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati String collection = StringUtils.hasText(collectionName) ? collectionName : getCollectionName(entityClass); String distanceField = operations.nearQueryDistanceFieldName(entityClass); + EntityProjection projection = operations.introspectProjection(returnType, + entityClass); GeoNearResultDocumentCallback callback = new GeoNearResultDocumentCallback<>(distanceField, - new ProjectingReadCallback<>(mongoConverter, entityClass, returnType, collection), near.getMetric()); + new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric()); Aggregation $geoNear = TypedAggregation.newAggregation(entityClass, Aggregation.geoNear(near, distanceField)) .withOptions(AggregationOptions.builder().collation(near.getCollation()).build()); @@ -1139,9 +1135,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityType); QueryContext queryContext = queryOperations.createQueryContext(query); + EntityProjection projection = operations.introspectProjection(resultType, + entityType); Document mappedQuery = queryContext.getMappedQuery(entity); - Document mappedFields = queryContext.getMappedFields(entity, resultType, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, projection); Document mappedSort = queryContext.getMappedSort(entity); return Mono.defer(() -> { @@ -1161,7 +1159,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati }).flatMap(it -> { Mono afterFindAndReplace = doFindAndReplace(it.getCollection(), mappedQuery, mappedFields, mappedSort, - queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), options, resultType); + queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), options, + projection); return afterFindAndReplace.flatMap(saved -> { maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection())); return maybeCallAfterSave(saved, it.getTarget(), it.getCollection()); @@ -2373,7 +2372,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati QueryContext queryContext = queryOperations .createQueryContext(new BasicQuery(query, fields != null ? fields : new Document())); - Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2425,7 +2425,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); - Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, + EntityProjection.nonProjecting(entityClass)); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2447,9 +2448,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Class targetClass, FindPublisherPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); + EntityProjection projection = operations.introspectProjection(targetClass, + sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); - Document mappedFields = queryContext.getMappedFields(entity, targetClass, projectionFactory); + Document mappedFields = queryContext.getMappedFields(entity, projection); Document mappedQuery = queryContext.getMappedQuery(entity); if (LOGGER.isDebugEnabled()) { @@ -2458,24 +2461,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer, - new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName); - } - - private Document getMappedFieldsObject(Document fields, @Nullable MongoPersistentEntity entity, - Class targetType) { - - if (entity == null) { - return fields; - } - - Document projectedFields = propertyOperations.computeFieldsForProjection(projectionFactory, fields, - entity.getType(), targetType); - - if (ObjectUtils.nullSafeEquals(fields, projectedFields)) { - return queryMapper.getMappedFields(projectedFields, entity); - } - - return queryMapper.getMappedFields(projectedFields, mappingContext.getRequiredPersistentEntity(targetType)); + new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); } protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) { @@ -2610,6 +2596,34 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { + EntityProjection projection = operations.introspectProjection(resultType, + entityType); + + return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, + options, projection); + } + + /** + * Customize this part for findAndReplace. + * + * @param collectionName The name of the collection to perform the operation in. + * @param mappedQuery the query to look up documents. + * @param mappedFields the fields to project the result to. + * @param mappedSort the sort to be applied when executing the query. + * @param collation collation settings for the query. Can be {@literal null}. + * @param entityType the source domain type. + * @param replacement the replacement {@link Document}. + * @param options applicable options. + * @param projection the projection descriptor. + * @return {@link Mono#empty()} if object does not exist, {@link FindAndReplaceOptions#isReturnNew() return new} is + * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. + * @since 3.4 + */ + private Mono doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, + Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, + EntityProjection projection) { + return Mono.defer(() -> { if (LOGGER.isDebugEnabled()) { @@ -2622,7 +2636,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return executeFindOneInternal( new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(this.mongoConverter, entityType, resultType, collectionName), collectionName); + new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName); }); } @@ -3203,37 +3217,30 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati */ private class ProjectingReadCallback implements DocumentCallback { - private final EntityReader reader; - private final Class entityType; - private final Class targetType; + private final MongoConverter reader; + private final EntityProjection projection; private final String collectionName; - ProjectingReadCallback(EntityReader reader, Class entityType, Class targetType, + ProjectingReadCallback(MongoConverter reader, EntityProjection projection, String collectionName) { this.reader = reader; - this.entityType = entityType; - this.targetType = targetType; + this.projection = projection; this.collectionName = collectionName; } @SuppressWarnings("unchecked") public Mono doWith(Document document) { - Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) // - ? entityType // - : targetType; - - maybeEmitEvent(new AfterLoadEvent<>(document, typeToRead, collectionName)); + Class returnType = projection.getMappedType().getType(); + maybeEmitEvent(new AfterLoadEvent<>(document, returnType, collectionName)); - Object entity = reader.read(typeToRead, document); + Object entity = reader.project(projection, document); if (entity == null) { throw new MappingException(String.format("EntityReader %s returned null", reader)); } - Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity; - - T castEntity = (T) result; + T castEntity = (T) entity; maybeEmitEvent(new AfterConvertEvent<>(document, castEntity, collectionName)); return maybeCallAfterConvert(castEntity, document, collectionName); } 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 0b31f7534..3afe41d1f 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 @@ -91,7 +91,7 @@ class DocumentAccessor { public void put(MongoPersistentProperty prop, @Nullable Object value) { Assert.notNull(prop, "MongoPersistentProperty must not be null!"); - String fieldName = prop.getFieldName(); + String fieldName = getFieldName(prop); if (!fieldName.contains(".")) { BsonUtils.addToMap(document, fieldName, value); @@ -123,7 +123,7 @@ class DocumentAccessor { */ @Nullable public Object get(MongoPersistentProperty property) { - return BsonUtils.resolveValue(document, property.getFieldName()); + return BsonUtils.resolveValue(document, getFieldName(property)); } /** @@ -150,7 +150,11 @@ class DocumentAccessor { Assert.notNull(property, "Property must not be null!"); - return BsonUtils.hasValue(document, property.getFieldName()); + return BsonUtils.hasValue(document, getFieldName(property)); + } + + String getFieldName(MongoPersistentProperty prop) { + return prop.getFieldName(); } /** 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 54d0abc4b..2a6fc00e2 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 @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -46,13 +47,20 @@ import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.annotation.Reference; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.TypeMapper; +import org.springframework.data.mapping.AccessOptions; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; @@ -69,6 +77,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentPropert import org.springframework.data.mongodb.core.mapping.DocumentPointer; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.PersistentPropertyTranslator; import org.springframework.data.mongodb.core.mapping.Unwrapped; import org.springframework.data.mongodb.core.mapping.Unwrapped.OnEmpty; import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; @@ -76,7 +85,10 @@ import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Predicates; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -129,6 +141,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private SpELContext spELContext; private @Nullable EntityCallbacks entityCallbacks; private final DocumentPointerFactory documentPointerFactory; + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); /** * Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. @@ -212,6 +225,16 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return this.typeMapper == null ? this.defaultTypeMapper : this.typeMapper; } + @Override + public ProjectionFactory getProjectionFactory() { + return projectionFactory; + } + + @Override + public CustomConversions getCustomConversions() { + return conversions; + } + /** * Configure the characters dots potentially contained in a {@link Map} shall be replaced with. By default we don't do * any translation but rather reject a {@link Map} with keys containing dots causing the conversion for the entire @@ -254,6 +277,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App this.applicationContext = applicationContext; this.spELContext = new SpELContext(this.spELContext, applicationContext); + this.projectionFactory.setBeanFactory(applicationContext); + this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); if (entityCallbacks == null) { setEntityCallbacks(EntityCallbacks.create(applicationContext)); @@ -281,6 +306,150 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App this.entityCallbacks = entityCallbacks; } + @Override + public R project(EntityProjection projection, Bson bson) { + + if (!projection.isProjection()) { // backed by real object + + TypeInformation typeToRead = projection.getMappedType().getType().isInterface() ? projection.getDomainType() + : projection.getMappedType(); + return (R) read(typeToRead, bson); + } + + ProjectingConversionContext context = new ProjectingConversionContext(conversions, ObjectPath.ROOT, + this::readCollectionOrArray, this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead, + projection); + + return doReadProjection(context, bson, projection); + } + + @SuppressWarnings("unchecked") + private R doReadProjection(ConversionContext context, Bson bson, + EntityProjection projection) { + + MongoPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(projection.getActualDomainType()); + TypeInformation mappedType = projection.getActualMappedType(); + MongoPersistentEntity mappedEntity = (MongoPersistentEntity) getMappingContext() + .getPersistentEntity(mappedType); + SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext); + + boolean isInterfaceProjection = mappedType.getType().isInterface(); + if (isInterfaceProjection) { + + PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(mappedEntity); + DocumentAccessor documentAccessor = new DocumentAccessor(bson); + PersistentPropertyAccessor accessor = new MapPersistentPropertyAccessor(); + + PersistentPropertyAccessor convertingAccessor = PropertyTranslatingPropertyAccessor + .create(new ConvertingPropertyAccessor<>(accessor, conversionService), propertyTranslator); + MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, + evaluator); + + readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, + Predicates.isTrue()); + return (R) projectionFactory.createProjection(mappedType.getType(), accessor.getBean()); + } + + // DTO projection + if (mappedEntity == null) { + throw new MappingException(String.format("No mapping metadata found for %s", mappedType.getType().getName())); + } + + // create target instance, merge metadata from underlying DTO type + PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(entity, + Predicates.negate(MongoPersistentProperty::hasExplicitFieldName)); + DocumentAccessor documentAccessor = new DocumentAccessor(bson) { + @Override + String getFieldName(MongoPersistentProperty prop) { + return propertyTranslator.translate(prop).getFieldName(); + } + }; + + PreferredConstructor persistenceConstructor = mappedEntity.getPersistenceConstructor(); + ParameterValueProvider provider = persistenceConstructor != null + && persistenceConstructor.hasParameters() + ? getParameterProvider(context, mappedEntity, documentAccessor, evaluator) + : NoOpParameterValueProvider.INSTANCE; + + EntityInstantiator instantiator = instantiators.getInstantiatorFor(mappedEntity); + R instance = instantiator.createInstance(mappedEntity, provider); + PersistentPropertyAccessor accessor = mappedEntity.getPropertyAccessor(instance); + + populateProperties(context, mappedEntity, documentAccessor, evaluator, instance); + + PersistentPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor<>(accessor, conversionService); + MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator); + + readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, evaluator, + Predicates.isTrue()); + + return accessor.getBean(); + } + + private Object doReadOrProject(ConversionContext context, Bson source, TypeInformation typeHint, + EntityProjection typeDescriptor) { + + if (typeDescriptor.isProjection()) { + return doReadProjection(context, BsonUtils.asDocument(source), typeDescriptor); + } + + return readDocument(context, source, typeHint); + } + + class ProjectingConversionContext extends ConversionContext { + + private final EntityProjection returnedTypeDescriptor; + + ProjectingConversionContext(CustomConversions customConversions, ObjectPath path, + ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, + ContainerValueConverter dbRefConverter, ValueConverter elementConverter, + EntityProjection projection) { + super(customConversions, path, + (context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection), + + collectionConverter, mapConverter, dbRefConverter, elementConverter); + this.returnedTypeDescriptor = projection; + } + + @Override + public ConversionContext forProperty(String name) { + + EntityProjection property = returnedTypeDescriptor.findProperty(name); + if (property == null) { + return super.forProperty(name); + } + + return new ProjectingConversionContext(conversions, path, collectionConverter, mapConverter, dbRefConverter, + elementConverter, property); + } + + @Override + public ConversionContext withPath(ObjectPath currentPath) { + return new ProjectingConversionContext(conversions, currentPath, collectionConverter, mapConverter, + dbRefConverter, elementConverter, returnedTypeDescriptor); + } + } + + static class MapPersistentPropertyAccessor implements PersistentPropertyAccessor> { + + Map map = new LinkedHashMap<>(); + + @Override + public void setProperty(PersistentProperty persistentProperty, Object o) { + map.put(persistentProperty.getName(), o); + } + + @Override + public Object getProperty(PersistentProperty persistentProperty) { + return map.get(persistentProperty.getName()); + } + + @Override + public Map getBean() { + return map; + } + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.core.MongoReader#read(java.lang.Class, com.mongodb.Document) @@ -423,7 +592,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor, evaluator); - readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator); + Predicate propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate(); + readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter); return accessor.getBean(); } @@ -465,19 +635,28 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private void readProperties(ConversionContext context, MongoPersistentEntity entity, PersistentPropertyAccessor accessor, DocumentAccessor documentAccessor, - MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator) { + MongoDbPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator, + Predicate propertyFilter) { DbRefResolverCallback callback = null; for (MongoPersistentProperty prop : entity) { + if (!propertyFilter.test(prop)) { + continue; + } + + ConversionContext propertyContext = context.forProperty(prop.getName()); + MongoDbPropertyValueProvider valueProviderToUse = valueProvider.withContext(propertyContext); + if (prop.isAssociation() && !entity.isConstructorArgument(prop)) { if (callback == null) { - callback = getDbRefResolverCallback(context, documentAccessor, evaluator); + callback = getDbRefResolverCallback(propertyContext, documentAccessor, evaluator); } - readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context, + readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, + propertyContext, evaluator); continue; } @@ -485,32 +664,27 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (prop.isUnwrapped()) { accessor.setProperty(prop, - readUnwrapped(context, documentAccessor, prop, mappingContext.getRequiredPersistentEntity(prop))); - continue; - } - - // We skip the id property since it was already set - - if (entity.isIdProperty(prop)) { + readUnwrapped(propertyContext, documentAccessor, prop, mappingContext.getRequiredPersistentEntity(prop))); continue; } - if (entity.isConstructorArgument(prop) || !documentAccessor.hasValue(prop)) { + if (!documentAccessor.hasValue(prop)) { continue; } if (prop.isAssociation()) { if (callback == null) { - callback = getDbRefResolverCallback(context, documentAccessor, evaluator); + callback = getDbRefResolverCallback(propertyContext, documentAccessor, evaluator); } - readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, context, + readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback, + propertyContext, evaluator); continue; } - accessor.setProperty(prop, valueProvider.getPropertyValue(prop)); + accessor.setProperty(prop, valueProviderToUse.getPropertyValue(prop)); } } @@ -1716,6 +1890,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return true; } + static Predicate isIdentifier(PersistentEntity entity) { + return entity::isIdProperty; + } + + static Predicate isConstructorArgument(PersistentEntity entity) { + return entity::isConstructorArgument; + } + /** * {@link PropertyValueProvider} to evaluate a SpEL expression if present on the property or simply accesses the field * of the configured source {@link Document}. @@ -1779,6 +1961,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return (T) context.convert(value, property.getTypeInformation()); } + + public MongoDbPropertyValueProvider withContext(ConversionContext context) { + if (context == this.context) { + return this; + } + + return new MongoDbPropertyValueProvider(context, accessor, evaluator); + + } } /** @@ -1997,13 +2188,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App */ protected static class ConversionContext { - private final org.springframework.data.convert.CustomConversions conversions; - private final ObjectPath path; - private final ContainerValueConverter documentConverter; - private final ContainerValueConverter> collectionConverter; - private final ContainerValueConverter mapConverter; - private final ContainerValueConverter dbRefConverter; - private final ValueConverter elementConverter; + final org.springframework.data.convert.CustomConversions conversions; + final ObjectPath path; + final ContainerValueConverter documentConverter; + final ContainerValueConverter> collectionConverter; + final ContainerValueConverter mapConverter; + final ContainerValueConverter dbRefConverter; + final ValueConverter elementConverter; ConversionContext(org.springframework.data.convert.CustomConversions customConversions, ObjectPath path, ContainerValueConverter documentConverter, ContainerValueConverter> collectionConverter, @@ -2099,6 +2290,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return path; } + public ConversionContext forProperty(String name) { + return this; + } + /** * Converts a simple {@code source} value into {@link TypeInformation the target type}. * @@ -2123,4 +2318,58 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } } + + private static class PropertyTranslatingPropertyAccessor implements PersistentPropertyPathAccessor { + + private final PersistentPropertyAccessor delegate; + private final PersistentPropertyTranslator propertyTranslator; + + private PropertyTranslatingPropertyAccessor(PersistentPropertyAccessor delegate, + PersistentPropertyTranslator propertyTranslator) { + this.delegate = delegate; + this.propertyTranslator = propertyTranslator; + } + + static PersistentPropertyAccessor create(PersistentPropertyAccessor delegate, + PersistentPropertyTranslator propertyTranslator) { + return new PropertyTranslatingPropertyAccessor<>(delegate, propertyTranslator); + } + + @Override + public void setProperty(PersistentProperty property, @Nullable Object value) { + delegate.setProperty(translate(property), value); + } + + @Override + public Object getProperty(PersistentProperty property) { + return delegate.getProperty(translate(property)); + } + + @Override + public T getBean() { + return delegate.getBean(); + } + + @Override + public void setProperty(PersistentPropertyPath> path, Object value, + AccessOptions.SetOptions options) { + throw new UnsupportedOperationException(); + } + + @Override + public Object getProperty(PersistentPropertyPath> path, + AccessOptions.GetOptions context) { + throw new UnsupportedOperationException(); + } + + @Override + public void setProperty(PersistentPropertyPath> path, Object value) { + throw new UnsupportedOperationException(); + } + + private MongoPersistentProperty translate(PersistentProperty property) { + return propertyTranslator.translate((MongoPersistentProperty) property); + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java index aff1b8d8e..c0a3e3178 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java @@ -19,13 +19,17 @@ import org.bson.BsonValue; import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; + import org.springframework.core.convert.ConversionException; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.EntityConverter; import org.springframework.data.convert.EntityReader; import org.springframework.data.convert.TypeMapper; +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -54,6 +58,35 @@ public interface MongoConverter */ MongoTypeMapper getTypeMapper(); + /** + * Returns the {@link ProjectionFactory} for this converter. + * + * @return will never be {@literal null}. + * @since 3.4 + */ + ProjectionFactory getProjectionFactory(); + + /** + * Returns the {@link CustomConversions} for this converter. + * + * @return will never be {@literal null}. + * @since 3.4 + */ + CustomConversions getCustomConversions(); + + /** + * Apply a projection to {@link Bson} and return the projection return type {@code R}. + * {@link EntityProjection#isProjection() Non-projecting} descriptors fall back to {@link #read(Class, Object) regular + * object materialization}. + * + * @param descriptor the projection descriptor, must not be {@literal null}. + * @param bson must not be {@literal null}. + * @param + * @return a new instance of the projection return type {@code R}. + * @since 3.4 + */ + R project(EntityProjection descriptor, Bson bson); + /** * Mapping function capable of converting values into a desired target type by eg. extracting the actual java type * from a given {@link BsonValue}. @@ -154,4 +187,5 @@ public interface MongoConverter return convertToMongoType(id,(TypeInformation) null); } } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 1ae0a1535..d97070aab 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -223,8 +223,8 @@ public class QueryMapper { if (fields.isEmpty()) { return BsonUtils.EMPTY_DOCUMENT; - } + Document target = new Document(); BsonUtils.asMap(filterUnwrappedObjects(fields, entity)).forEach((k, v) -> { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java index 66c88cf9b..80d10c414 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java @@ -201,7 +201,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope * {@link org.springframework.data.mongodb.core.mapping.Field#value()} present. * @since 1.7 */ - protected boolean hasExplicitFieldName() { + public boolean hasExplicitFieldName() { return StringUtils.hasText(getAnnotatedFieldName()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java index 8dc89e03f..cb31bdb74 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java @@ -41,6 +41,13 @@ public interface MongoPersistentProperty extends PersistentProperty + * Mainly used within the framework. + * + * @author Mark Paluch + * @since 3.4 + */ +public class PersistentPropertyTranslator { + + /** + * Translate a {@link MongoPersistentProperty} into a corresponding property from a different + * {@link MongoPersistentEntity}. + * + * @param property must not be {@literal null}. + * @return the translated property. Can be the original {@code property}. + */ + public MongoPersistentProperty translate(MongoPersistentProperty property) { + return property; + } + + /** + * Create a new {@link PersistentPropertyTranslator}. + * + * @param targetEntity must not be {@literal null}. + * @return the property translator to use. + */ + public static PersistentPropertyTranslator create(@Nullable MongoPersistentEntity targetEntity) { + return create(targetEntity, Predicates.isTrue()); + } + + /** + * Create a new {@link PersistentPropertyTranslator} accepting a {@link Predicate filter predicate} whether the + * translation should happen at all. + * + * @param targetEntity must not be {@literal null}. + * @param translationFilter must not be {@literal null}. + * @return the property translator to use. + */ + public static PersistentPropertyTranslator create(@Nullable MongoPersistentEntity targetEntity, + Predicate translationFilter) { + return targetEntity != null ? new EntityPropertyTranslator(targetEntity, translationFilter) + : new PersistentPropertyTranslator(); + } + + private static class EntityPropertyTranslator extends PersistentPropertyTranslator { + + private final MongoPersistentEntity targetEntity; + private final Predicate translationFilter; + + EntityPropertyTranslator(MongoPersistentEntity targetEntity, + Predicate translationFilter) { + this.targetEntity = targetEntity; + this.translationFilter = translationFilter; + } + + @Override + public MongoPersistentProperty translate(MongoPersistentProperty property) { + + if (!translationFilter.test(property)) { + return property; + } + + MongoPersistentProperty targetProperty = targetEntity.getPersistentProperty(property.getName()); + return targetProperty != null ? targetProperty : property; + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java index 24e4ae057..7ad3e0074 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java @@ -56,6 +56,12 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty { return context.getProperty().findAnnotation(Unwrapped.class).prefix() + delegate.getFieldName(); } + @Override + public boolean hasExplicitFieldName() { + return delegate.hasExplicitFieldName() + || !ObjectUtils.isEmpty(context.getProperty().findAnnotation(Unwrapped.class).prefix()); + } + @Override public Class getFieldType() { return delegate.getFieldType(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 5a023b2b0..836f04b6e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; +import org.springframework.beans.factory.BeanFactory; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; @@ -76,6 +77,11 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { this.mappingContext = mongoOperations.getConverter().getMappingContext(); } + @Override + protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + return this.operations.getConverter().getProjectionFactory(); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 996985907..6476550be 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; +import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; @@ -78,6 +79,11 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); } + @Override + protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + return this.operations.getConverter().getProjectionFactory(); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java index d24a8e802..4b26ba69a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java @@ -24,6 +24,8 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; /** @@ -37,7 +39,7 @@ public class EntityOperationUnitTests { @BeforeEach public void setUp() { - ops = new EntityOperations(mappingContext); + ops = new EntityOperations(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)); } @Test // DATAMONGO-2293 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java index 160a598bc..9c6b750c3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java @@ -22,7 +22,8 @@ import java.time.Instant; import org.junit.jupiter.api.Test; import org.springframework.data.mapping.MappingException; -import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.TimeSeries; import org.springframework.data.mongodb.test.util.MongoTestMappingContext; @@ -33,7 +34,8 @@ import org.springframework.data.mongodb.test.util.MongoTestMappingContext; */ class EntityOperationsUnitTests { - EntityOperations operations = new EntityOperations(MongoTestMappingContext.newTestContext()); + EntityOperations operations = new EntityOperations( + new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, MongoTestMappingContext.newTestContext())); @Test // GH-3731 void shouldReportInvalidTimeField() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java index 8ebf72e13..540334a4d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java @@ -21,7 +21,9 @@ import static org.springframework.data.mongodb.core.query.Query.*; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import java.util.Date; import java.util.stream.Stream; @@ -32,6 +34,7 @@ import org.bson.Document; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -569,6 +572,8 @@ class ExecutableFindOperationSupportTests { String getName(); } + @Getter + @Setter // TODO: Without getters/setters, not identified as projection/properties static class PersonDtoProjection { @Field("firstname") String name; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index d103b7361..f772fd462 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -27,6 +27,7 @@ import org.bson.BsonString; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Query; @@ -278,7 +279,6 @@ class ExecutableUpdateOperationSupportTests { @Data static class Jedi { - @Field("firstname") String name; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java index 3bbff3fcd..24d000773 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoOperationsUnitTests.java @@ -27,9 +27,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.dao.DataAccessException; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.geo.Point; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.AbstractMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -37,6 +40,7 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.NearQuery; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.util.TypeInformation; import com.mongodb.DBRef; @@ -92,6 +96,21 @@ public abstract class MongoOperationsUnitTests { public MongoTypeMapper getTypeMapper() { return null; } + + @Override + public ProjectionFactory getProjectionFactory() { + return null; + } + + @Override + public CustomConversions getCustomConversions() { + return null; + } + + @Override + public R project(EntityProjection descriptor, Bson bson) { + return null; + } }; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 3d6278d4b..34a43c910 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -102,6 +102,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -410,6 +411,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { when(cursor.next()).thenReturn(new org.bson.Document("_id", Integer.valueOf(0))); MappingMongoConverter converter = mock(MappingMongoConverter.class); when(converter.getMappingContext()).thenReturn((MappingContext) mappingContext); + when(converter.getProjectionFactory()).thenReturn(new SpelAwareProxyProjectionFactory()); template = new MongoTemplate(factory, converter); assertThatExceptionOfType(MappingException.class).isThrownBy(() -> template.findAll(Person.class)) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 10e4f1cfc..9ee424421 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -90,6 +90,7 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.timeseries.Granularity; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -1151,6 +1152,7 @@ public class ReactiveMongoTemplateUnitTests { MappingMongoConverter converter = mock(MappingMongoConverter.class); when(converter.getMappingContext()).thenReturn((MappingContext) mappingContext); + when(converter.getProjectionFactory()).thenReturn(new SpelAwareProxyProjectionFactory()); template = new ReactiveMongoTemplate(factory, converter); when(collection.find(Document.class)).thenReturn(findPublisher); @@ -1480,7 +1482,6 @@ public class ReactiveMongoTemplateUnitTests { AutogenerateableId foo; } - static class PersonExtended extends Person { String lastname; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java index 4d1d5a426..989ea8b60 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java @@ -48,8 +48,8 @@ class UpdateOperationsUnitTests { MongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); QueryMapper queryMapper = new QueryMapper(mongoConverter); UpdateMapper updateMapper = new UpdateMapper(mongoConverter); - EntityOperations entityOperations = new EntityOperations(mappingContext); - PropertyOperations propertyOperations = new PropertyOperations(mappingContext); + EntityOperations entityOperations = new EntityOperations(mongoConverter); + PropertyOperations propertyOperations = new PropertyOperations(mongoConverter.getMappingContext()); ExtendedQueryOperations queryOperations = new ExtendedQueryOperations(queryMapper, updateMapper, entityOperations, propertyOperations, MongoClientSettings::getDefaultCodecRegistry); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java index b1d4a6120..fcc10e49a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverterUnitTests.java @@ -19,13 +19,17 @@ import static org.mockito.Mockito.*; import org.bson.conversions.Bson; import org.junit.jupiter.api.Test; + import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.mapping.context.EntityProjection; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToStringConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.StringToObjectIdConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.util.TypeInformation; import com.mongodb.DBRef; @@ -59,6 +63,21 @@ public class AbstractMongoConverterUnitTests { throw new UnsupportedOperationException(); } + @Override + public ProjectionFactory getProjectionFactory() { + return null; + } + + @Override + public CustomConversions getCustomConversions() { + return null; + } + + @Override + public R project(EntityProjection descriptor, Bson bson) { + return null; + } + @Override public MappingContext, MongoPersistentProperty> getMappingContext() { throw new UnsupportedOperationException(); 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 d4c735fd2..a5200c5ec 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 @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.DocumentTestUtils.*; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.RequiredArgsConstructor; import java.math.BigDecimal; @@ -66,6 +67,8 @@ import org.springframework.data.geo.Polygon; import org.springframework.data.geo.Shape; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.mapping.context.EntityProjection; +import org.springframework.data.mapping.context.EntityProjectionIntrospector; import org.springframework.data.mapping.model.MappingInstantiationException; import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType; @@ -138,7 +141,7 @@ class MappingMongoConverterUnitTests { converter.write(address, document); assertThat(document.get("city").toString()).isEqualTo("New York"); - assertThat(document.get("street").toString()).isEqualTo("Broadway"); + assertThat(document.get("s").toString()).isEqualTo("Broadway"); } @Test @@ -2191,7 +2194,8 @@ class MappingMongoConverterUnitTests { @Test // GH-3546 void readFlattensNestedDocumentToStringIfNecessary() { - org.bson.Document source = new org.bson.Document("street", new org.bson.Document("json", "string").append("_id", UUID.randomUUID())); + org.bson.Document source = new org.bson.Document("s", + new org.bson.Document("json", "string").append("_id", UUID.randomUUID())); Address target = converter.read(Address.class, source); assertThat(target.street).isNotNull(); @@ -2355,7 +2359,7 @@ class MappingMongoConverterUnitTests { void readUnwrappedTypeWithComplexValue() { org.bson.Document source = new org.bson.Document("_id", "id-1").append("address", - new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham")); + new org.bson.Document("s", "1007 Mountain Drive").append("city", "Gotham")); WithNullableUnwrapped target = converter.read(WithNullableUnwrapped.class, source); @@ -2381,9 +2385,9 @@ class MappingMongoConverterUnitTests { converter.write(source, target); assertThat(target) // - .containsEntry("address", new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham")) // + .containsEntry("address", new org.bson.Document("s", "1007 Mountain Drive").append("city", "Gotham")) // .doesNotContainKey("street") // - .doesNotContainKey("address.street") // + .doesNotContainKey("address.s") // .doesNotContainKey("city") // .doesNotContainKey("address.city"); } @@ -2636,6 +2640,80 @@ class MappingMongoConverterUnitTests { assertThat(accessor.getDocument()).isEqualTo(new org.bson.Document("pName", new org.bson.Document("_id", id.toString()))); } + @Test // GH-2860 + void projectShouldReadSimpleInterfaceProjection() { + + org.bson.Document source = new org.bson.Document("birthDate", new LocalDate(1999, 12, 1).toDate()).append("foo", + "Walter"); + + EntityProjectionIntrospector discoverer = EntityProjectionIntrospector.create(converter.getProjectionFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjection projection = discoverer + .introspect(PersonProjection.class, Person.class); + PersonProjection person = converter.project(projection, source); + + assertThat(person.getBirthDate()).isEqualTo(new LocalDate(1999, 12, 1)); + assertThat(person.getFirstname()).isEqualTo("Walter"); + } + + @Test // GH-2860 + void projectShouldReadSimpleDtoProjection() { + + org.bson.Document source = new org.bson.Document("birthDate", new LocalDate(1999, 12, 1).toDate()).append("foo", + "Walter"); + + EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjection projection = introspector + .introspect(PersonDto.class, Person.class); + PersonDto person = converter.project(projection, source); + + assertThat(person.getBirthDate()).isEqualTo(new LocalDate(1999, 12, 1)); + assertThat(person.getFirstname()).isEqualTo("Walter"); + } + + @Test // GH-2860 + void projectShouldReadNestedProjection() { + + org.bson.Document source = new org.bson.Document("addresses", + Collections.singletonList(new org.bson.Document("s", "hwy"))); + + EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjection projection = introspector + .introspect(WithNestedProjection.class, Person.class); + WithNestedProjection person = converter.project(projection, source); + + assertThat(person.getAddresses()).extracting(AddressProjection::getStreet).hasSize(1).containsOnly("hwy"); + } + + @Test // GH-2860 + void projectShouldReadProjectionWithNestedEntity() { + + org.bson.Document source = new org.bson.Document("addresses", + Collections.singletonList(new org.bson.Document("s", "hwy"))); + + EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create(converter.getProjectionFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !converter.conversions.isSimpleType(target)), + mappingContext); + + EntityProjection projection = introspector + .introspect(ProjectionWithNestedEntity.class, Person.class); + ProjectionWithNestedEntity person = converter.project(projection, source); + + assertThat(person.getAddresses()).extracting(Address::getStreet).hasSize(1).containsOnly("hwy"); + } + static class GenericType { T content; } @@ -2666,7 +2744,9 @@ class MappingMongoConverterUnitTests { } @EqualsAndHashCode + @Getter static class Address implements InterfaceType { + @Field("s") String street; String city; } @@ -2696,6 +2776,54 @@ class MappingMongoConverterUnitTests { } } + interface PersonProjection { + + LocalDate getBirthDate(); + + String getFirstname(); + } + + interface WithNestedProjection { + + Set getAddresses(); + } + + interface ProjectionWithNestedEntity { + + Set
getAddresses(); + } + + interface AddressProjection { + + String getStreet(); + } + + static class PersonDto { + + LocalDate birthDate; + + @Field("foo") String firstname; + String lastname; + + public PersonDto(LocalDate birthDate, String firstname, String lastname) { + this.birthDate = birthDate; + this.firstname = firstname; + this.lastname = lastname; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public String getFirstname() { + return firstname; + } + + public String getLastname() { + return lastname; + } + } + static class ClassWithSortedMap { SortedMap map; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java index 625f0c199..263da4e74 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java @@ -25,6 +25,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoTemplate; @@ -32,6 +34,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; /** @@ -40,6 +43,7 @@ import org.springframework.data.repository.Repository; * @author Oliver Gierke */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class MongoRepositoryFactoryUnitTests { @Mock MongoTemplate template; @@ -55,6 +59,7 @@ public class MongoRepositoryFactoryUnitTests { public void setUp() { when(template.getConverter()).thenReturn(converter); when(converter.getMappingContext()).thenReturn(mappingContext); + when(converter.getProjectionFactory()).thenReturn(new SpelAwareProxyProjectionFactory()); } @Test