From ee59c6b7748471ed82fd0bc30169a374606709b7 Mon Sep 17 00:00:00 2001 From: Roman Puchkovskiy Date: Mon, 17 Feb 2020 22:43:25 +0400 Subject: [PATCH] DATAMONGO-2479 - Add AfterConvertCallback and AfterSaveCallback (and their reactive versions). Previously, only BeforeConvertCallback and BeforeSaveCallback were supported (and their reactive counterparts). This commit adds support for 'after-convert' and 'after-save' events using entity callbacks feature. Original pull request: #839. --- .../mongodb/core/DefaultBulkOperations.java | 26 ++ .../data/mongodb/core/MongoTemplate.java | 100 +++--- .../mongodb/core/ReactiveMongoTemplate.java | 169 ++++----- .../core/convert/MappingMongoConverter.java | 43 ++- .../mapping/event/AfterConvertCallback.java | 40 +++ .../core/mapping/event/AfterSaveCallback.java | 40 +++ .../event/ReactiveAfterConvertCallback.java | 43 +++ .../event/ReactiveAfterSaveCallback.java | 44 +++ .../core/DefaultBulkOperationsUnitTests.java | 23 +- .../mongodb/core/MongoTemplateUnitTests.java | 315 +++++++++++++++++ .../core/ReactiveMongoTemplateUnitTests.java | 322 ++++++++++++++++++ .../MappingMongoConverterUnitTests.java | 69 ++++ 12 files changed, 1095 insertions(+), 139 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterConvertCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterSaveCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterConvertCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterSaveCallback.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java index ee5741035..9c553f277 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java @@ -32,6 +32,7 @@ import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback; import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; @@ -62,6 +63,7 @@ import com.mongodb.client.model.*; * @author Minsu Kim * @author Jens Schauder * @author Michail Nikolaev + * @author Roman Puchkovskiy * @since 1.9 */ class DefaultBulkOperations implements BulkOperations { @@ -300,6 +302,7 @@ class DefaultBulkOperations implements BulkOperations { Assert.state(result != null, "Result must not be null."); models.forEach(this::maybeEmitAfterSaveEvent); + models.forEach(this::maybeInvokeAfterSaveCallback); return result; } finally { @@ -447,6 +450,19 @@ class DefaultBulkOperations implements BulkOperations { } } + private void maybeInvokeAfterSaveCallback(SourceAwareWriteModelHolder it) { + + if (it.getModel() instanceof InsertOneModel) { + + Document target = ((InsertOneModel) it.getModel()).getDocument(); + maybeInvokeAfterSaveCallback(it.getSource(), target); + } else if (it.getModel() instanceof ReplaceOneModel) { + + Document target = ((ReplaceOneModel) it.getModel()).getReplacement(); + maybeInvokeAfterSaveCallback(it.getSource(), target); + } + } + private , T> E maybeEmitEvent(E event) { if (null != bulkOperationContext.getEventPublisher()) { @@ -475,6 +491,16 @@ class DefaultBulkOperations implements BulkOperations { collectionName); } + private Object maybeInvokeAfterSaveCallback(Object value, Document mappedDocument) { + + if (bulkOperationContext.getEntityCallbacks() == null) { + return value; + } + + return bulkOperationContext.getEntityCallbacks().callback(AfterSaveCallback.class, value, mappedDocument, + collectionName); + } + private static BulkWriteOptions getBulkWriteOptions(BulkMode bulkMode) { BulkWriteOptions options = new BulkWriteOptions(); 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 07a4e2bd6..20055ebbf 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 @@ -33,6 +33,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -70,16 +71,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; -import org.springframework.data.mongodb.core.convert.DbRefResolver; -import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; -import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; -import org.springframework.data.mongodb.core.convert.MongoConverter; -import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; -import org.springframework.data.mongodb.core.convert.MongoWriter; -import org.springframework.data.mongodb.core.convert.QueryMapper; -import org.springframework.data.mongodb.core.convert.UpdateMapper; +import org.springframework.data.mongodb.core.convert.*; import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.index.IndexOperationsProvider; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; @@ -87,16 +79,7 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; -import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback; -import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; +import org.springframework.data.mongodb.core.mapping.event.*; import org.springframework.data.mongodb.core.mapreduce.GroupBy; import org.springframework.data.mongodb.core.mapreduce.GroupByResults; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; @@ -128,16 +111,7 @@ import com.mongodb.ClientSessionOptions; import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; -import com.mongodb.client.AggregateIterable; -import com.mongodb.client.ClientSession; -import com.mongodb.client.DistinctIterable; -import com.mongodb.client.FindIterable; -import com.mongodb.client.MapReduceIterable; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoCursor; -import com.mongodb.client.MongoDatabase; -import com.mongodb.client.MongoIterable; +import com.mongodb.client.*; import com.mongodb.client.model.*; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -166,6 +140,7 @@ import com.mongodb.client.result.UpdateResult; * @author Andreas Zink * @author Cimon Lucas * @author Michael J. Simons + * @author Roman Puchkovskiy */ public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider { @@ -1070,8 +1045,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); maybeCallBeforeSave(replacement, mappedReplacement, collectionName); - return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, + T saved = doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, resultType); + if (saved != null) { + maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName)); + return maybeCallAfterSave(saved, mappedReplacement, collectionName); + } + + return saved; } // Find methods that take a Query to express the query and that return a single object that is also removed from the @@ -1233,8 +1214,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, T saved = populateIdIfNecessary(initialized, id); maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); - - return saved; + return maybeCallAfterSave(saved, dbDoc, collectionName); } @Override @@ -1327,8 +1307,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, if (i < ids.size()) { T saved = populateIdIfNecessary(obj, ids.get(i)); - maybeEmitEvent(new AfterSaveEvent<>(saved, documentList.get(i), collectionName)); - savedObjects.add(saved); + Document doc = documentList.get(i); + maybeEmitEvent(new AfterSaveEvent<>(saved, doc, collectionName)); + savedObjects.add(maybeCallAfterSave(saved, doc, collectionName)); } else { savedObjects.add(obj); } @@ -1398,7 +1379,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } maybeEmitEvent(new AfterSaveEvent<>(toSave, mapped.getDocument(), collectionName)); - return toSave; + return maybeCallAfterSave(toSave, mapped.getDocument(), collectionName); } protected T doSave(String collectionName, T objectToSave, MongoWriter writer) { @@ -1419,7 +1400,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, T saved = populateIdIfNecessary(objectToSave, id); maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); - return saved; + return maybeCallAfterSave(saved, dbDoc, collectionName); } @SuppressWarnings("ConstantConditions") @@ -2312,7 +2293,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return event; } - @SuppressWarnings("unchecked") protected T maybeCallBeforeConvert(T object, String collection) { if (null != entityCallbacks) { @@ -2322,7 +2302,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return object; } - @SuppressWarnings("unchecked") protected T maybeCallBeforeSave(T object, Document document, String collection) { if (null != entityCallbacks) { @@ -2332,6 +2311,24 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return object; } + protected T maybeCallAfterSave(T object, Document document, String collection) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(AfterSaveCallback.class, object, document, collection); + } + + return object; + } + + protected T maybeCallAfterConvert(T object, Document document, String collection) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(AfterConvertCallback.class, object, document, collection); + } + + return object; + } + /** * Create the specified collection using the provided options * @@ -3101,6 +3098,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * * @author Oliver Gierke * @author Christoph Strobl + * @author Roman Puchkovskiy */ @RequiredArgsConstructor private class ReadDocumentCallback implements DocumentCallback { @@ -3110,16 +3108,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final String collectionName; @Nullable - public T doWith(@Nullable Document object) { + public T doWith(@Nullable Document document) { - if (null != object) { - maybeEmitEvent(new AfterLoadEvent<>(object, type, collectionName)); + if (null != document) { + maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName)); } - T source = reader.read(type, object); + T source = reader.read(type, document); if (null != source) { - maybeEmitEvent(new AfterConvertEvent<>(object, source, collectionName)); + maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName)); + source = maybeCallAfterConvert(source, document, collectionName); } return source; @@ -3148,24 +3147,25 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @SuppressWarnings("unchecked") @Nullable - public T doWith(@Nullable Document object) { + public T doWith(@Nullable Document document) { - if (object == null) { + if (document == null) { return null; } Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType : targetType; - if (null != object) { - maybeEmitEvent(new AfterLoadEvent<>(object, targetType, collectionName)); + if (null != document) { + maybeEmitEvent(new AfterLoadEvent<>(document, targetType, collectionName)); } - Object source = reader.read(typeToRead, object); + Object source = reader.read(typeToRead, document); Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source; if (null != result) { - maybeEmitEvent(new AfterConvertEvent<>(object, result, collectionName)); + maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName)); + result = maybeCallAfterConvert(result, document, collectionName); } return (T) result; 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 b2f457132..b65afdb51 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 @@ -74,16 +74,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.PrefixingDelegatingAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; -import org.springframework.data.mongodb.core.convert.DbRefResolver; -import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; -import org.springframework.data.mongodb.core.convert.MongoConverter; -import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; -import org.springframework.data.mongodb.core.convert.MongoWriter; -import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; -import org.springframework.data.mongodb.core.convert.QueryMapper; -import org.springframework.data.mongodb.core.convert.UpdateMapper; +import org.springframework.data.mongodb.core.convert.*; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; import org.springframework.data.mongodb.core.index.ReactiveIndexOperations; import org.springframework.data.mongodb.core.index.ReactiveMongoPersistentEntityIndexCreator; @@ -91,16 +82,7 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; -import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; -import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; -import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; +import org.springframework.data.mongodb.core.mapping.event.*; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Meta; @@ -127,16 +109,7 @@ import com.mongodb.CursorType; import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; -import com.mongodb.client.model.CountOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.DeleteOptions; -import com.mongodb.client.model.FindOneAndDeleteOptions; -import com.mongodb.client.model.FindOneAndReplaceOptions; -import com.mongodb.client.model.FindOneAndUpdateOptions; -import com.mongodb.client.model.ReplaceOptions; -import com.mongodb.client.model.ReturnDocument; -import com.mongodb.client.model.UpdateOptions; -import com.mongodb.client.model.ValidationOptions; +import com.mongodb.client.model.*; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.InsertOneResult; @@ -163,6 +136,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase; * * @author Mark Paluch * @author Christoph Strobl + * @author Roman Puchkovskiy * @since 2.0 */ public class ReactiveMongoTemplate implements ReactiveMongoOperations, ApplicationContextAware { @@ -1050,7 +1024,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati cursor = cursor.maxTime(options.getMaxTime().toMillis(), TimeUnit.MILLISECONDS); } - return Flux.from(cursor).map(readCallback::doWith); + return Flux.from(cursor).concatMap(readCallback::doWith); } /* @@ -1093,7 +1067,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati .withOptions(AggregationOptions.builder().collation(near.getCollation()).build()); return aggregate($geoNear, collection, Document.class) // - .map(callback::doWith); + .concatMap(callback::doWith); } /* @@ -1186,9 +1160,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati }).flatMap(it -> { PersistableEntityModel flowObject = (PersistableEntityModel) it; - return doFindAndReplace(flowObject.getCollection(), mappedQuery, mappedFields, mappedSort, - queryContext.getCollation(entityType).orElse(null), entityType, flowObject.getTarget(), options, - resultType); + Mono afterFindAndReplace = doFindAndReplace(flowObject.getCollection(), mappedQuery, + mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), + entityType, flowObject.getTarget(), options, resultType); + return afterFindAndReplace.flatMap(saved -> { + maybeEmitEvent(new AfterSaveEvent<>(saved, flowObject.getTarget(), flowObject.getCollection())); + return maybeCallAfterSave(saved, flowObject.getTarget(), flowObject.getCollection()); + }); }); } @@ -1345,12 +1323,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati }).flatMap(it -> { - return insertDocument(it.getCollection(), it.getTarget(), it.getSource().getClass()).map(id -> { + return insertDocument(it.getCollection(), it.getTarget(), it.getSource().getClass()).flatMap(id -> { T saved = operations.forEntity(it.getSource(), mongoConverter.getConversionService()) .populateIdIfNecessary(id); maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), collectionName)); - return saved; + return maybeCallAfterSave(saved, it.getTarget(), collectionName); }); }); } @@ -1436,13 +1414,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return insertDocumentList(collectionName, documents).thenMany(Flux.fromIterable(tuples)); }); - return insertDocuments.map(tuple -> { + return insertDocuments.flatMap(tuple -> { - Object id = MappedDocument.of(tuple.getT2()).getId(); + Document document = tuple.getT2(); + Object id = MappedDocument.of(document).getId(); T saved = tuple.getT1().populateIdIfNecessary(id); - maybeEmitEvent(new AfterSaveEvent<>(saved, tuple.getT2(), collectionName)); - return saved; + maybeEmitEvent(new AfterSaveEvent<>(saved, document, collectionName)); + return maybeCallAfterSave(saved, document, collectionName); }); } @@ -1522,9 +1501,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati maybeEmitEvent(new BeforeSaveEvent<>(toConvert, document, collectionName)); return maybeCallBeforeSave(toConvert, document, collectionName).flatMap(it -> { - return doUpdate(collectionName, query, mapped.updateWithoutId(), it.getClass(), false, false).map(result -> { - return maybeEmitEvent(new AfterSaveEvent(it, document, collectionName)).getSource(); - }); + return doUpdate(collectionName, query, mapped.updateWithoutId(), it.getClass(), false, false) + .flatMap(result -> { + maybeEmitEvent(new AfterSaveEvent(it, document, collectionName)); + return maybeCallAfterSave(it, document, collectionName); + }); }); }); }); @@ -1546,10 +1527,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return maybeCallBeforeSave(toConvert, dbDoc, collectionName).flatMap(it -> { - return saveDocument(collectionName, dbDoc, it.getClass()).map(id -> { + return saveDocument(collectionName, dbDoc, it.getClass()).flatMap(id -> { T saved = entity.populateIdIfNecessary(id); - return maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)).getSource(); + maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); + return maybeCallAfterSave(saved, dbDoc, collectionName); }); }); }); @@ -2213,7 +2195,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati publisher = collation.map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher); return Flux.from(publisher) - .map(new ReadDocumentCallback<>(mongoConverter, resultType, inputCollectionName)::doWith); + .concatMap(new ReadDocumentCallback<>(mongoConverter, resultType, inputCollectionName)::doWith); }); } @@ -2613,7 +2595,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return event; } - @SuppressWarnings("unchecked") protected Mono maybeCallBeforeConvert(T object, String collection) { if (null != entityCallbacks) { @@ -2623,7 +2604,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return Mono.just(object); } - @SuppressWarnings("unchecked") protected Mono maybeCallBeforeSave(T object, Document document, String collection) { if (null != entityCallbacks) { @@ -2633,6 +2613,24 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return Mono.just(object); } + protected Mono maybeCallAfterSave(T object, Document document, String collection) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(ReactiveAfterSaveCallback.class, object, document, collection); + } + + return Mono.just(object); + } + + protected Mono maybeCallAfterConvert(T object, Document document, String collection) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(ReactiveAfterConvertCallback.class, object, document, collection); + } + + return Mono.just(object); + } + private MongoCollection getAndPrepareCollection(MongoDatabase db, String collectionName) { try { @@ -2720,7 +2718,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati DocumentCallback objectCallback, String collectionName) { return createMono(collectionName, - collection -> Mono.from(collectionCallback.doInCollection(collection)).map(objectCallback::doWith)); + collection -> Mono.from(collectionCallback.doInCollection(collection)).flatMap(objectCallback::doWith)); } /** @@ -2746,7 +2744,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return createFlux(collectionName, collection -> { return Flux.from(preparer.initiateFind(collection, collectionCallback::doInCollection)) - .map(objectCallback::doWith); + .concatMap(objectCallback::doWith); }); } @@ -3042,7 +3040,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati interface DocumentCallback { - T doWith(Document object); + Mono doWith(Document object); } /** @@ -3071,6 +3069,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * {@link EntityReader}. * * @author Mark Paluch + * @author Roman Puchkovskiy */ class ReadDocumentCallback implements DocumentCallback { @@ -3088,27 +3087,33 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.collectionName = collectionName; } - public T doWith(@Nullable Document object) { + public Mono doWith(Document document) { - if (null != object) { - maybeEmitEvent(new AfterLoadEvent<>(object, type, collectionName)); - } - T source = reader.read(type, object); + maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName)); + + T source = reader.read(type, document); if (null != source) { - maybeEmitEvent(new AfterConvertEvent<>(object, source, collectionName)); + maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName)); } - return source; + return Mono.defer(() -> { + if (null != source) { + return maybeCallAfterConvert(source, document, collectionName); + } else { + return Mono.empty(); + } + }); } } /** - * {@link MongoTemplate.DocumentCallback} transforming {@link Document} into the given {@code targetType} or + * {@link DocumentCallback} transforming {@link Document} into the given {@code targetType} or * decorating the {@code sourceType} with a {@literal projection} in case the {@code targetType} is an - * {@litera interface}. + * {@literal interface}. * * @param * @param * @author Christoph Strobl + * @author Roman Puchkovskiy * @since 2.0 */ @RequiredArgsConstructor @@ -3119,29 +3124,30 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final @NonNull Class targetType; private final @NonNull String collectionName; - @Nullable @SuppressWarnings("unchecked") - public T doWith(@Nullable Document object) { - - if (object == null) { - return null; - } + public Mono doWith(Document document) { Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) // ? entityType // : targetType; - if (null != object) { - maybeEmitEvent(new AfterLoadEvent<>(object, typeToRead, collectionName)); - } + maybeEmitEvent(new AfterLoadEvent<>(document, typeToRead, collectionName)); - Object source = reader.read(typeToRead, object); + Object source = reader.read(typeToRead, document); Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source; - if (null != source) { - maybeEmitEvent(new AfterConvertEvent<>(object, result, collectionName)); + T castEntity = (T) result; + if (null != castEntity) { + maybeEmitEvent(new AfterConvertEvent<>(document, castEntity, collectionName)); } - return (T) result; + + return Mono.defer(() -> { + if (null != castEntity) { + return maybeCallAfterConvert(castEntity, document, collectionName); + } else { + return Mono.empty(); + } + }); } } @@ -3151,6 +3157,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * * @author Mark Paluch * @author Chrstoph Strobl + * @author Roman Puchkovskiy */ static class GeoNearResultDocumentCallback implements DocumentCallback> { @@ -3175,16 +3182,17 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.metric = metric; } - public GeoResult doWith(Document object) { + public Mono> doWith(Document object) { - double distance = Double.NaN; + final double distance; if (object.containsKey(distanceField)) { distance = NumberUtils.convertNumberToTargetClass(object.get(distanceField, Number.class), Double.class); + } else { + distance = Double.NaN; } - - T doWith = delegate.doWith(object); - - return new GeoResult<>(doWith, new Distance(distance, metric)); + + return delegate.doWith(object) + .map(doWith -> new GeoResult<>(doWith, new Distance(distance, metric))); } } @@ -3202,7 +3210,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.type = type; } - @SuppressWarnings("deprecation") public FindPublisher prepare(FindPublisher findPublisher) { FindPublisher findPublisherToUse = operations.forType(type) // 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 6dfb990bf..51306aec4 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 @@ -50,6 +50,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; 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.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; @@ -64,6 +65,7 @@ import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; 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; @@ -91,6 +93,7 @@ import com.mongodb.DBRef; * @author Christoph Strobl * @author Jordi Llach * @author Mark Paluch + * @author Roman Puchkovskiy */ public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware { @@ -110,6 +113,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App protected @Nullable CodecRegistryProvider codecRegistryProvider; private SpELContext spELContext; + private @Nullable EntityCallbacks entityCallbacks; /** * Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}. @@ -212,6 +216,26 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App this.applicationContext = applicationContext; this.spELContext = new SpELContext(this.spELContext, applicationContext); + + if (entityCallbacks == null) { + setEntityCallbacks(EntityCallbacks.create(applicationContext)); + } + } + + /** + * Set the {@link EntityCallbacks} instance to use when invoking + * {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link AfterConvertCallback}. + *

+ * Overrides potentially existing {@link EntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 3.0 + */ + public void setEntityCallbacks(EntityCallbacks entityCallbacks) { + + Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!"); + this.entityCallbacks = entityCallbacks; } /* @@ -1605,7 +1629,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App : bulkReadRefs(dbrefs); String collectionName = dbrefs.iterator().next().getCollectionName(); - List targeList = new ArrayList<>(dbrefs.size()); + List targetList = new ArrayList<>(dbrefs.size()); for (Document document : referencedRawDocuments) { @@ -1613,15 +1637,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App maybeEmitEvent(new AfterLoadEvent<>(document, (Class) rawType, collectionName)); } - final T target = (T) read(type, document, path); - targeList.add(target); + T target = (T) read(type, document, path); if (target != null) { maybeEmitEvent(new AfterConvertEvent<>(document, target, collectionName)); + target = maybeCallAfterConvert(target, document, collectionName); } + + targetList.add(target); } - return targeList; + return targetList; } private void maybeEmitEvent(MongoMappingEvent event) { @@ -1635,6 +1661,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return this.applicationContext != null; } + protected T maybeCallAfterConvert(T object, Document document, String collection) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(AfterConvertCallback.class, object, document, collection); + } + + return object; + } + /** * Performs the fetch operation for the given {@link DBRef}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterConvertCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterConvertCallback.java new file mode 100644 index 000000000..3dc4f0417 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterConvertCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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.mapping.event; + +import org.bson.Document; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Callback being invoked after a domain object is converted from a Document (when reading from the DB). + * + * @author Roman Puchkovskiy + * @since 3.0 + */ +@FunctionalInterface +public interface AfterConvertCallback extends EntityCallback { + + /** + * Entity callback method invoked after a domain object is converted from a Document. Can return either the same + * or a modified instance of the domain object. + * + * @param entity the domain object (the result of the conversion). + * @param document must not be {@literal null}. + * @param collection name of the collection. + * @return the domain object that is the result of the conversion from the Document. + */ + T onAfterConvert(T entity, Document document, String collection); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterSaveCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterSaveCallback.java new file mode 100644 index 000000000..c22597ca4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterSaveCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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.mapping.event; + +import org.bson.Document; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Entity callback triggered after save of a document. + * + * @author Roman Puchkovskiy + * @since 3.0 + */ +@FunctionalInterface +public interface AfterSaveCallback extends EntityCallback { + + /** + * Entity callback method invoked after a domain object is saved. Can return either the same or a modified instance + * of the domain object. + * + * @param entity the domain object that was saved. + * @param document {@link Document} representing the {@code entity}. + * @param collection name of the collection. + * @return the domain object that was persisted. + */ + T onAfterSave(T entity, Document document, String collection); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterConvertCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterConvertCallback.java new file mode 100644 index 000000000..7a273cf15 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterConvertCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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.mapping.event; + +import org.bson.Document; +import org.reactivestreams.Publisher; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; + +/** + * Callback being invoked after a domain object is converted from a Document (when reading from the DB). + * + * @author Roman Puchkovskiy + * @since 3.0 + * @see ReactiveEntityCallbacks + */ +@FunctionalInterface +public interface ReactiveAfterConvertCallback extends EntityCallback { + + /** + * Entity callback method invoked after a domain object is converted from a Document. Can return either the same + * or a modified instance of the domain object. + * + * @param entity the domain object (the result of the conversion). + * @param document must not be {@literal null}. + * @param collection name of the collection. + * @return a {@link Publisher} emitting the domain object that is the result of the conversion from the Document. + */ + Publisher onAfterConvert(T entity, Document document, String collection); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterSaveCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterSaveCallback.java new file mode 100644 index 000000000..64eda738f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAfterSaveCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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.mapping.event; + +import org.bson.Document; +import org.reactivestreams.Publisher; + +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; + +/** + * Entity callback triggered after save of a document. + * + * @author Roman Puchkovskiy + * @since 3.0 + * @see ReactiveEntityCallbacks + */ +@FunctionalInterface +public interface ReactiveAfterSaveCallback extends EntityCallback { + + /** + * Entity callback method invoked after a domain object is saved. Can return either the same or a modified instance + * of the domain object. + * + * @param entity the domain object that was saved. + * @param document {@link Document} representing the {@code entity}. + * @param collection name of the collection. + * @return a {@link Publisher} emitting the domain object to be returned to the caller. + */ + Publisher onAfterSave(T entity, Document document, String collection); +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java index 47302d2ac..48682071d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java @@ -54,6 +54,7 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback; import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; @@ -84,6 +85,7 @@ import com.mongodb.client.model.WriteModel; * @author Mark Paluch * @author Minsu Kim * @author Jens Schauder + * @author Roman Puchkovskiy */ @ExtendWith(MockitoExtension.class) class DefaultBulkOperationsUnitTests { @@ -208,16 +210,17 @@ class DefaultBulkOperationsUnitTests { assertThat(updateModel.getReplacement().getString("lastName")).isEqualTo("Kim"); } - @Test // DATAMONGO-2261 + @Test // DATAMONGO-2261, DATAMONGO-2479 void bulkInsertInvokesEntityCallbacks() { BeforeConvertPersonCallback beforeConvertCallback = spy(new BeforeConvertPersonCallback()); BeforeSavePersonCallback beforeSaveCallback = spy(new BeforeSavePersonCallback()); + AfterSavePersonCallback afterSaveCallback = spy(new AfterSavePersonCallback()); ops = new DefaultBulkOperations(template, "collection-1", new BulkOperationContext(BulkMode.ORDERED, Optional.of(mappingContext.getPersistentEntity(Person.class)), new QueryMapper(converter), new UpdateMapper(converter), null, - EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback))); + EntityCallbacks.create(beforeConvertCallback, beforeSaveCallback, afterSaveCallback))); Person entity = new Person("init"); ops.insert(entity); @@ -229,11 +232,13 @@ class DefaultBulkOperationsUnitTests { ops.execute(); verify(beforeSaveCallback).onBeforeSave(personArgumentCaptor.capture(), any(), eq("collection-1")); - assertThat(personArgumentCaptor.getAllValues()).extracting("firstName").containsExactly("init", "before-convert"); + verify(afterSaveCallback).onAfterSave(personArgumentCaptor.capture(), any(), eq("collection-1")); + assertThat(personArgumentCaptor.getAllValues()).extracting("firstName") + .containsExactly("init", "before-convert", "before-convert"); verify(collection).bulkWrite(captor.capture(), any()); InsertOneModel updateModel = (InsertOneModel) captor.getValue().get(0); - assertThat(updateModel.getDocument()).containsEntry("firstName", "before-save"); + assertThat(updateModel.getDocument()).containsEntry("firstName", "after-save"); } @Test // DATAMONGO-2290 @@ -365,6 +370,16 @@ class DefaultBulkOperationsUnitTests { } } + static class AfterSavePersonCallback implements AfterSaveCallback { + + @Override + public Person onAfterSave(Person entity, Document document, String collection) { + + document.put("firstName", "after-save"); + return new Person("after-save"); + } + } + static class NullExceptionTranslator implements PersistenceExceptionTranslator { @Override 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 3d87afad8..5539f5391 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 @@ -25,7 +25,9 @@ import java.math.BigInteger; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -37,6 +39,7 @@ import org.assertj.core.api.Assertions; import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -81,6 +84,9 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback; @@ -97,10 +103,13 @@ import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; +import com.google.common.collect.ImmutableMap; import com.mongodb.MongoClientSettings; import com.mongodb.MongoException; import com.mongodb.MongoNamespace; import com.mongodb.ReadPreference; +import com.mongodb.ServerAddress; +import com.mongodb.ServerCursor; import com.mongodb.WriteConcern; import com.mongodb.client.AggregateIterable; import com.mongodb.client.DistinctIterable; @@ -129,6 +138,7 @@ import com.mongodb.client.result.UpdateResult; * @author Christoph Strobl * @author Mark Paluch * @author Michael J. Simons + * @author Roman Puchkovskiy */ @MockitoSettings(strictness = Strictness.LENIENT) public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @@ -1921,6 +1931,241 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { verify(findIterable).projection(new Document("country", 1).append("userid", 1)); } + @Test // DATAMONGO-2479 + public void findShouldInvokeAfterConvertCallback() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(findIterable.iterator()).thenReturn(new OneElementCursor<>(document)); + + template.find(new Query(), Person.class); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + } + + private Document initialLukeDocument() { + return new Document(ImmutableMap.of( + "_id", "init", + "firstname", "luke" + )); + } + + private Person initialLuke() { + Person expectedEnitty = new Person(); + expectedEnitty.id = "init"; + expectedEnitty.firstname = "luke"; + return expectedEnitty; + } + + @Test // DATAMONGO-2479 + public void findByIdShouldInvokeAfterConvertCallback() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(findIterable.first()).thenReturn(document); + + template.findById("init", Person.class); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + } + + @Test // DATAMONGO-2479 + public void findOneShouldInvokeAfterConvertCallback() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(findIterable.first()).thenReturn(document); + + template.findOne(new Query(), Person.class); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + } + + @Test // DATAMONGO-2479 + public void findAllShouldInvokeAfterConvertCallback() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(findIterable.iterator()).thenReturn(new OneElementCursor<>(document)); + + template.findAll(Person.class); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + } + + @Test // DATAMONGO-2479 + public void findAndModifyShouldInvokeAfterConvertCallback() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.findOneAndUpdate(any(Bson.class), any(Bson.class), any())).thenReturn(document); + + template.findAndModify(new Query(), new Update(), Person.class); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + } + + @Test // DATAMONGO-2479 + public void findAndRemoveShouldInvokeAfterConvertCallback() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.findOneAndDelete(any(Bson.class), any())).thenReturn(document); + + template.findAndRemove(new Query(), Person.class); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + } + + @Test // DATAMONGO-2479 + public void findAllAndRemoveShouldInvokeAfterConvertCallback() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(findIterable.iterator()).thenReturn(new OneElementCursor<>(document)); + + template.findAllAndRemove(new Query(), Person.class); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + } + + @Test // DATAMONGO-2479 + public void findAndReplaceShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + Person entity = initialLuke(); + + Document document = initialLukeDocument(); + when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(document); + + Person saved = template.findAndReplace(new Query(), entity); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(saved.id).isEqualTo("after-convert"); + } + + @Test // DATAMONGO-2479 + public void saveShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback)); + + Person entity = initialLuke(); + + Person saved = template.save(entity); + + verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString()); + assertThat(saved.id).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void insertShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback)); + + Person entity = initialLuke(); + + Person saved = template.insert(entity); + + verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString()); + assertThat(saved.id).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void insertAllShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback)); + + Person entity1 = new Person(); + entity1.id = "1"; + entity1.firstname = "luke"; + + Person entity2 = new Person(); + entity1.id = "2"; + entity1.firstname = "luke"; + + Collection saved = template.insertAll(Arrays.asList(entity1, entity2)); + + verify(afterSaveCallback, times(2)).onAfterSave(any(), any(), anyString()); + assertThat(saved.iterator().next().getId()).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void findAndReplaceShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(EntityCallbacks.create(afterSaveCallback)); + + Person entity = initialLuke(); + + Document document = initialLukeDocument(); + when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(document); + + Person saved = template.findAndReplace(new Query(), entity); + + verify(afterSaveCallback).onAfterSave(eq(initialLuke()), any(), anyString()); + assertThat(saved.id).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void findAndReplaceShouldEmitAfterSaveEvent() { + + AbstractMongoEventListener eventListener = new AbstractMongoEventListener() { + + @Override + public void onAfterSave(AfterSaveEvent event) { + + assertThat(event.getSource().id).isEqualTo("init"); + event.getSource().id = "after-save-event"; + } + }; + + StaticApplicationContext ctx = new StaticApplicationContext(); + ctx.registerBean(ApplicationListener.class, () -> eventListener); + ctx.refresh(); + + template.setApplicationContext(ctx); + + Person entity = initialLuke(); + + Document document = initialLukeDocument(); + when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(document); + + Person saved = template.findAndReplace(new Query(), entity); + + assertThat(saved.id).isEqualTo("after-save-event"); + } + class AutogenerateableId { @Id BigInteger id; @@ -2097,4 +2342,74 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { return entity; } } + + static class ValueCapturingAfterSaveCallback extends ValueCapturingEntityCallback + implements AfterSaveCallback { + + @Override + public Person onAfterSave(Person entity, Document document, String collection) { + + capture(entity); + return new Person() {{ + id = "after-save"; + firstname = entity.firstname; + }}; + } + } + + static class ValueCapturingAfterConvertCallback extends ValueCapturingEntityCallback + implements AfterConvertCallback { + + @Override + public Person onAfterConvert(Person entity, Document document, String collection) { + + capture(entity); + return new Person() {{ + id = "after-convert"; + firstname = entity.firstname; + }}; + } + } + + static class OneElementCursor implements MongoCursor { + private final Iterator iterator; + + OneElementCursor(T element) { + iterator = Collections.singletonList(element).iterator(); + } + + @Override + public void close() { + // nothing to close + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + return iterator.next(); + } + + @Override + public T tryNext() { + if (iterator.hasNext()) { + return iterator.next(); + } else { + return null; + } + } + + @Override + public ServerCursor getServerCursor() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public ServerAddress getServerAddress() { + throw new IllegalStateException("Not implemented"); + } + } } 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 e34f4922c..f95c5fe67 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 @@ -21,6 +21,7 @@ import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import lombok.Data; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -32,6 +33,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.assertj.core.api.Assertions; import org.bson.Document; @@ -46,9 +48,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; import org.springframework.context.support.StaticApplicationContext; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; @@ -65,6 +69,10 @@ import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; +import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; @@ -78,6 +86,7 @@ import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; +import com.google.common.collect.ImmutableMap; import com.mongodb.MongoClientSettings; import com.mongodb.ReadPreference; import com.mongodb.client.model.CountOptions; @@ -88,6 +97,9 @@ import com.mongodb.client.model.FindOneAndReplaceOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.InsertManyResult; +import com.mongodb.client.result.InsertOneResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.reactivestreams.client.AggregatePublisher; import com.mongodb.reactivestreams.client.DistinctPublisher; @@ -102,6 +114,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase; * * @author Mark Paluch * @author Christoph Strobl + * @author Roman Puchkovskiy */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -1096,6 +1109,286 @@ public class ReactiveMongoTemplateUnitTests { verify(findPublisher).projection(new Document("country", 1).append("userid", 1)); } + @Test // DATAMONGO-2479 + public void findShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.find(Document.class)).thenReturn(findPublisher); + makeFindPublisherPublishJust(document); + + List results = template.find(new Query(), Person.class).timeout(Duration.ofSeconds(1)) + .toStream().collect(Collectors.toList()); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(results.get(0).id).isEqualTo("after-convert"); + } + + private Document initialLukeDocument() { + return new Document(ImmutableMap.of( + "_id", "init", + "firstname", "luke" + )); + } + + private Person initialLuke() { + Person expectedEnitty = new Person(); + expectedEnitty.id = "init"; + expectedEnitty.firstname = "luke"; + return expectedEnitty; + } + + @Test // DATAMONGO-2479 + public void findByIdShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.find(any(Bson.class), eq(Document.class))).thenReturn(findPublisher); + makeFindPublisherPublishJust(document); + + Person result = template.findById("init", Person.class).block(Duration.ofSeconds(1)); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(result.id).isEqualTo("after-convert"); + } + + @Test // DATAMONGO-2479 + public void findOneShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.find(any(Bson.class), eq(Document.class))).thenReturn(findPublisher); + makeFindPublisherPublishJust(document); + + Person result = template.findOne(new Query(), Person.class).block(Duration.ofSeconds(1)); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(result.id).isEqualTo("after-convert"); + } + + @Test // DATAMONGO-2479 + public void findAllShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.find(Document.class)).thenReturn(findPublisher); + makeFindPublisherPublishJust(document); + + List results = template.findAll(Person.class).timeout(Duration.ofSeconds(1)) + .toStream().collect(Collectors.toList()); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(results.get(0).id).isEqualTo("after-convert"); + } + + @Test // DATAMONGO-2479 + public void findAndModifyShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.findOneAndUpdate(any(Bson.class), any(Bson.class), any())).thenReturn(findPublisher); + makeFindPublisherPublishJust(document); + + Person result = template.findAndModify(new Query(), new Update(), Person.class).block(Duration.ofSeconds(1)); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(result.id).isEqualTo("after-convert"); + } + + @Test // DATAMONGO-2479 + public void findAndRemoveShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.findOneAndDelete(any(Bson.class), any())).thenReturn(findPublisher); + makeFindPublisherPublishJust(document); + + Person result = template.findAndRemove(new Query(), Person.class).block(Duration.ofSeconds(1)); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(result.id).isEqualTo("after-convert"); + } + + @Test // DATAMONGO-2479 + public void findAllAndRemoveShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + Document document = initialLukeDocument(); + when(collection.find(Document.class)).thenReturn(findPublisher); + makeFindPublisherPublishJust(document); + when(collection.deleteMany(any(Bson.class), any(DeleteOptions.class))) + .thenReturn(Mono.just(spy(DeleteResult.class))); + + List results = template.findAllAndRemove(new Query(), Person.class).timeout(Duration.ofSeconds(1)) + .toStream().collect(Collectors.toList()); + + verify(afterConvertCallback).onAfterConvert(eq(initialLuke()), eq(document), anyString()); + assertThat(results.get(0).id).isEqualTo("after-convert"); + } + + private void makeFindPublisherPublishJust(Document document) { + + Publisher realPublisher = Flux.just(document); + + doAnswer(invocation -> { + Subscriber subscriber = invocation.getArgument(0); + realPublisher.subscribe(subscriber); + return null; + }).when(findPublisher).subscribe(any()); + } + + @Test // DATAMONGO-2479 + public void findAndReplaceShouldInvokeAfterConvertCallbacks() { + + ValueCapturingAfterConvertCallback afterConvertCallback = spy(new ValueCapturingAfterConvertCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterConvertCallback)); + + when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(findPublisher); + makeFindPublisherPublishJust(initialLukeDocument()); + + Person entity = new Person(); + entity.id = "init"; + entity.firstname = "luke"; + + Person saved = template.findAndReplace(new Query(), entity).block(Duration.ofSeconds(1)); + + verify(afterConvertCallback).onAfterConvert(eq(entity), any(), anyString()); + assertThat(saved.id).isEqualTo("after-convert"); + } + + @Test // DATAMONGO-2479 + public void saveShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback)); + + when(collection.replaceOne(any(Bson.class), any(Document.class), any(ReplaceOptions.class))) + .thenReturn(Mono.just(mock(UpdateResult.class))); + + Person entity = initialLuke(); + + Person saved = template.save(entity).block(Duration.ofSeconds(1)); + + verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString()); + assertThat(saved.id).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void insertShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback)); + + when(collection.insertOne(any())).thenReturn(Mono.just(mock(InsertOneResult.class))); + + Person entity = initialLuke(); + + Person saved = template.insert(entity).block(Duration.ofSeconds(1)); + + verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString()); + assertThat(saved.id).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void insertAllShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback)); + + Person entity1 = new Person(); + entity1.id = "1"; + entity1.firstname = "luke"; + + Person entity2 = new Person(); + entity1.id = "2"; + entity1.firstname = "luke"; + + when(collection.insertMany(anyList())).then(invocation -> { + List list = invocation.getArgument(0); + return Flux.fromIterable(list).map(i -> mock(InsertManyResult.class)); + }); + + List saved = template.insertAll(Arrays.asList(entity1, entity2)) + .timeout(Duration.ofSeconds(1)) + .toStream().collect(Collectors.toList()); + + verify(afterSaveCallback, times(2)).onAfterSave(any(), any(), anyString()); + assertThat(saved.get(0).id).isEqualTo("after-save"); + assertThat(saved.get(1).id).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void findAndReplaceShouldInvokeAfterSaveCallbacks() { + + ValueCapturingAfterSaveCallback afterSaveCallback = spy(new ValueCapturingAfterSaveCallback()); + + template.setEntityCallbacks(ReactiveEntityCallbacks.create(afterSaveCallback)); + + when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(findPublisher); + makeFindPublisherPublishJust(initialLukeDocument()); + + Person entity = initialLuke(); + + Person saved = template.findAndReplace(new Query(), entity).block(Duration.ofSeconds(1)); + + verify(afterSaveCallback).onAfterSave(eq(entity), any(), anyString()); + assertThat(saved.id).isEqualTo("after-save"); + } + + @Test // DATAMONGO-2479 + public void findAndReplaceShouldEmitAfterSaveEvent() { + + AbstractMongoEventListener eventListener = new AbstractMongoEventListener() { + + @Override + public void onAfterSave(AfterSaveEvent event) { + + assertThat(event.getSource().id).isEqualTo("init"); + event.getSource().id = "after-save-event"; + } + }; + + StaticApplicationContext ctx = new StaticApplicationContext(); + ctx.registerBean(ApplicationListener.class, () -> eventListener); + ctx.refresh(); + + template.setApplicationContext(ctx); + + Person entity = initialLuke(); + + Document document = initialLukeDocument(); + when(collection.findOneAndReplace(any(Bson.class), any(Document.class), any())).thenReturn(Mono.just(document)); + + Person saved = template.findAndReplace(new Query(), entity).block(Duration.ofSeconds(1)); + + assertThat(saved.id).isEqualTo("after-save-event"); + } + @Data @org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars") static class Person { @@ -1179,4 +1472,33 @@ public class ReactiveMongoTemplateUnitTests { return Mono.just(entity); } } + + static class ValueCapturingAfterConvertCallback extends ValueCapturingEntityCallback + implements ReactiveAfterConvertCallback { + + @Override + public Mono onAfterConvert(Person entity, Document document, String collection) { + + capture(entity); + return Mono.just(new Person() {{ + id = "after-convert"; + firstname = entity.firstname; + }}); + } + } + + static class ValueCapturingAfterSaveCallback extends ValueCapturingEntityCallback + implements ReactiveAfterSaveCallback { + + @Override + public Mono onAfterSave(Person entity, Document document, String collection) { + + capture(entity); + return Mono.just(new Person() {{ + id = "after-save"; + firstname = entity.firstname; + }}); + } + } + } 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 c2af8c856..cdc0a02b4 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 @@ -30,6 +30,7 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.*; +import org.assertj.core.api.Assertions; import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; @@ -46,6 +47,7 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; +import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; @@ -62,6 +64,7 @@ import org.springframework.data.geo.Point; 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.model.MappingInstantiationException; import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType; @@ -75,6 +78,7 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.PersonPojoStringId; import org.springframework.data.mongodb.core.mapping.TextScore; +import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; import org.springframework.data.util.ClassTypeInformation; import org.springframework.test.util.ReflectionTestUtils; @@ -90,6 +94,7 @@ import com.mongodb.DBRef; * @author Patrik Wasik * @author Christoph Strobl * @author Mark Paluch + * @author Roman Puchkovskiy */ @ExtendWith(MockitoExtension.class) public class MappingMongoConverterUnitTests { @@ -2103,6 +2108,62 @@ public class MappingMongoConverterUnitTests { .isEqualTo(new BasicDBObject("property", "value")); } + @Test // DATAMONGO-2479 + public void entityCallbacksAreNotSetByDefault() { + Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isNull(); + } + + @Test // DATAMONGO-2479 + public void entityCallbacksShouldBeInitiatedOnSettingApplicationContext() { + + ApplicationContext ctx = new StaticApplicationContext(); + converter.setApplicationContext(ctx); + + Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isNotNull(); + } + + @Test // DATAMONGO-2479 + public void setterForEntityCallbackOverridesContextInitializedOnes() { + + ApplicationContext ctx = new StaticApplicationContext(); + converter.setApplicationContext(ctx); + + EntityCallbacks callbacks = EntityCallbacks.create(); + converter.setEntityCallbacks(callbacks); + + Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isSameAs(callbacks); + } + + @Test // DATAMONGO-2479 + public void setterForApplicationContextShouldNotOverrideAlreadySetEntityCallbacks() { + + EntityCallbacks callbacks = EntityCallbacks.create(); + ApplicationContext ctx = new StaticApplicationContext(); + + converter.setEntityCallbacks(callbacks); + converter.setApplicationContext(ctx); + + Assertions.assertThat(ReflectionTestUtils.getField(converter, "entityCallbacks")).isSameAs(callbacks); + } + + @Test // DATAMONGO-2479 + public void resolveDBRefMapValueShouldInvokeCallbacks() { + + AfterConvertCallback afterConvertCallback = spy(new ReturningAfterConvertCallback()); + converter.setEntityCallbacks(EntityCallbacks.create(afterConvertCallback)); + + when(resolver.fetch(Mockito.any(DBRef.class))).thenReturn(new org.bson.Document()); + DBRef dbRef = mock(DBRef.class); + + org.bson.Document refMap = new org.bson.Document("foo", dbRef); + org.bson.Document document = new org.bson.Document("personMap", refMap); + + DBRefWrapper result = converter.read(DBRefWrapper.class, document); + + verify(afterConvertCallback).onAfterConvert(eq(result.personMap.get("foo")), + eq(new org.bson.Document()), any()); + } + static class GenericType { T content; } @@ -2565,4 +2626,12 @@ public class MappingMongoConverterUnitTests { Date dateAsObjectId; } + static class ReturningAfterConvertCallback implements AfterConvertCallback { + + @Override + public Person onAfterConvert(Person entity, org.bson.Document document, String collection) { + + return entity; + } + } }