From 45bd6d544d6ffdafc95c525f8edd790f0d9e8fab Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 16 Apr 2019 15:50:23 +0200 Subject: [PATCH] DATAMONGO-2261 - Use Entity Callback API for auditing. We now use EntityCallback to invoke callback actions on entities before saving/before conversion to provide hooks that potentially modify an entity before persisting it. We also provide a reactive variant of entity callbacks allowing to consume Reactor Context and to defer the actual activity. Original Pull Request: #742 --- .../MongoAuditingBeanDefinitionParser.java | 38 +- .../config/MongoAuditingRegistrar.java | 27 +- .../data/mongodb/core/MongoTemplate.java | 49 +- .../mongodb/core/ReactiveMongoTemplate.java | 116 ++- .../mapping/event/AuditingEntityCallback.java | 65 ++ .../mapping/event/AuditingEventListener.java | 2 + .../mapping/event/BeforeConvertCallback.java | 40 + .../mapping/event/BeforeSaveCallback.java | 44 ++ .../event/ReactiveAuditingEntityCallback.java | 68 ++ .../event/ReactiveBeforeConvertCallback.java | 42 ++ .../event/ReactiveBeforeSaveCallback.java | 45 ++ .../main/resources/META-INF/spring.schemas | 3 +- .../data/mongodb/config/spring-mongo-2.2.xsd | 687 ++++++++++++++++++ .../config/AuditingIntegrationTests.java | 16 +- .../AuditingEntityCallbackUnitTests.java | 137 ++++ 15 files changed, 1328 insertions(+), 51 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAuditingEntityCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeConvertCallback.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java create mode 100644 spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallbackUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java index 92ad1ad2f..ba1bfd533 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.config; import static org.springframework.data.config.ParsingUtils.*; import static org.springframework.data.mongodb.config.BeanNames.*; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -26,25 +27,33 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.data.auditing.config.IsNewAwareAuditingHandlerBeanDefinitionParser; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.data.mongodb.core.mapping.event.AuditingEventListener; +import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback; +import org.springframework.data.mongodb.core.mapping.event.ReactiveAuditingEntityCallback; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; + import org.w3c.dom.Element; /** - * {@link BeanDefinitionParser} to register a {@link AuditingEventListener} to transparently set auditing information on - * an entity. + * {@link BeanDefinitionParser} to register a {@link AuditingEntityCallback} to transparently set auditing information + * on an entity. * * @author Oliver Gierke + * @author Mark Paluch */ public class MongoAuditingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono", + MongoAuditingRegistrar.class.getClassLoader()); + /* * (non-Javadoc) * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) */ @Override protected Class getBeanClass(Element element) { - return AuditingEventListener.class; + return AuditingEntityCallback.class; } /* @@ -80,7 +89,24 @@ public class MongoAuditingBeanDefinitionParser extends AbstractSingleBeanDefinit mappingContextRef); parser.parse(element, parserContext); - builder.addConstructorArgValue(getObjectFactoryBeanDefinition(parser.getResolvedBeanName(), - parserContext.extractSource(element))); + AbstractBeanDefinition isNewAwareAuditingHandler = getObjectFactoryBeanDefinition(parser.getResolvedBeanName(), + parserContext.extractSource(element)); + builder.addConstructorArgValue(isNewAwareAuditingHandler); + + if (PROJECT_REACTOR_AVAILABLE) { + registerReactiveAuditingEntityCallback(parserContext.getRegistry(), isNewAwareAuditingHandler, + parserContext.extractSource(element)); + } + } + + private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, + AbstractBeanDefinition isNewAwareAuditingHandler, @Nullable Object source) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); + + builder.addConstructorArgValue(isNewAwareAuditingHandler); + builder.getRawBeanDefinition().setSource(source); + + registry.registerBeanDefinition(ReactiveAuditingEntityCallback.class.getName(), builder.getBeanDefinition()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java index f8d04a313..a87aa30a5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java @@ -32,17 +32,23 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.core.mapping.event.AuditingEventListener; +import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback; +import org.springframework.data.mongodb.core.mapping.event.ReactiveAuditingEntityCallback; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link ImportBeanDefinitionRegistrar} to enable {@link EnableMongoAuditing} annotation. * * @author Thomas Darimont * @author Oliver Gierke + * @author Mark Paluch */ class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono", + MongoAuditingRegistrar.class.getClassLoader()); + /* * (non-Javadoc) * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() @@ -104,12 +110,27 @@ class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); BeanDefinitionBuilder listenerBeanDefinitionBuilder = BeanDefinitionBuilder - .rootBeanDefinition(AuditingEventListener.class); + .rootBeanDefinition(AuditingEntityCallback.class); listenerBeanDefinitionBuilder .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), - AuditingEventListener.class.getName(), registry); + AuditingEntityCallback.class.getName(), registry); + + if (PROJECT_REACTOR_AVAILABLE) { + registerReactiveAuditingEntityCallback(registry, auditingHandlerDefinition.getSource()); + } + } + + private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, Object source) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); + + builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + builder.getRawBeanDefinition().setSource(source); + + registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingEntityCallback.class.getName(), + registry); } /** 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 258716318..13d5d62fa 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 @@ -56,6 +56,7 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; +import org.springframework.data.mapping.callback.SimpleEntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.MongoDbFactory; @@ -92,8 +93,10 @@ 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.mapreduce.GroupBy; @@ -140,7 +143,17 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.*; +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.ValidationAction; +import com.mongodb.client.model.ValidationLevel; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -201,6 +214,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private WriteResultChecking writeResultChecking = WriteResultChecking.NONE; private @Nullable ReadPreference readPreference; private @Nullable ApplicationEventPublisher eventPublisher; + private @Nullable SimpleEntityCallbacks entityCallbacks; private @Nullable ResourceLoader resourceLoader; private @Nullable MongoPersistentEntityIndexCreator indexCreator; @@ -346,6 +360,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, eventPublisher = applicationContext; + entityCallbacks = new SimpleEntityCallbacks(applicationContext); + if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } @@ -1245,6 +1261,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, BeforeConvertEvent event = new BeforeConvertEvent<>(objectToSave, collectionName); T toConvert = maybeEmitEvent(event).getSource(); + toConvert = maybeCallBeforeConvert(toConvert, collectionName); AdaptibleEntity entity = operations.forEntity(toConvert, mongoConverter.getConversionService()); entity.assertUpdateableIdIfNotSet(); @@ -1253,6 +1270,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document dbDoc = entity.toMappedDocument(writer).getDocument(); maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName)); + initialized = maybeCallBeforeSave(initialized, dbDoc, collectionName); Object id = insertDocument(collectionName, dbDoc, initialized.getClass()); T saved = populateIdIfNecessary(initialized, id); @@ -1332,6 +1350,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, BeforeConvertEvent event = new BeforeConvertEvent<>(uninitialized, collectionName); T toConvert = maybeEmitEvent(event).getSource(); + toConvert = maybeCallBeforeConvert(toConvert, collectionName); AdaptibleEntity entity = operations.forEntity(toConvert, mongoConverter.getConversionService()); entity.assertUpdateableIdIfNotSet(); @@ -1339,6 +1358,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, T initialized = entity.initializeVersionProperty(); Document document = entity.toMappedDocument(writer).getDocument(); maybeEmitEvent(new BeforeSaveEvent<>(initialized, document, collectionName)); + initialized = maybeCallBeforeSave(initialized, document, collectionName); documentList.add(document); initializedBatchToSave.add(initialized); @@ -1399,12 +1419,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, T toSave = source.incrementVersion(); toSave = maybeEmitEvent(new BeforeConvertEvent(toSave, collectionName)).getSource(); + toSave = maybeCallBeforeConvert(toSave, collectionName); source.assertUpdateableIdIfNotSet(); MappedDocument mapped = source.toMappedDocument(mongoConverter); maybeEmitEvent(new BeforeSaveEvent<>(toSave, mapped.getDocument(), collectionName)); + toSave = maybeCallBeforeSave(toSave, mapped.getDocument(), collectionName); UpdateDefinition update = mapped.updateWithoutId(); UpdateResult result = doUpdate(collectionName, query, update, toSave.getClass(), false, false); @@ -1423,6 +1445,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, protected T doSave(String collectionName, T objectToSave, MongoWriter writer) { objectToSave = maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName)).getSource(); + objectToSave = maybeCallBeforeConvert(objectToSave, collectionName); AdaptibleEntity entity = operations.forEntity(objectToSave, mongoConverter.getConversionService()); entity.assertUpdateableIdIfNotSet(); @@ -1431,6 +1454,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document dbDoc = mapped.getDocument(); maybeEmitEvent(new BeforeSaveEvent<>(objectToSave, dbDoc, collectionName)); + objectToSave = maybeCallBeforeSave(objectToSave, dbDoc, collectionName); Object id = saveDocument(collectionName, dbDoc, objectToSave.getClass()); T saved = populateIdIfNecessary(entity.getBean(), id); @@ -2312,6 +2336,28 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return event; } + @SuppressWarnings("unchecked") + protected T maybeCallBeforeConvert(T object, String collection) { + + if (null != entityCallbacks) { + return (T) entityCallbacks.callback(object, BeforeConvertCallback.class, + (cb, t) -> cb.onBeforeConvert(t, collection)); + } + + return object; + } + + @SuppressWarnings("unchecked") + protected T maybeCallBeforeSave(T object, Document document, String collection) { + + if (null != entityCallbacks) { + return (T) entityCallbacks.callback(object, BeforeSaveCallback.class, + (cb, t) -> cb.onBeforeSave(t, document, collection)); + } + + return object; + } + /** * Create the specified collection using the provided options * @@ -2634,6 +2680,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } maybeEmitEvent(new BeforeSaveEvent<>(replacement, replacement, collectionName)); + replacement = maybeCallBeforeSave(replacement, replacement, collectionName); return executeFindOneInternal( new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), 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 5fa010121..7495793f9 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 @@ -59,6 +59,7 @@ import org.springframework.data.geo.Metric; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDbFactory; @@ -97,6 +98,8 @@ 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.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Meta; @@ -198,6 +201,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private WriteResultChecking writeResultChecking = WriteResultChecking.NONE; private @Nullable ReadPreference readPreference; private @Nullable ApplicationEventPublisher eventPublisher; + private @Nullable ReactiveEntityCallbacks entityCallbacks; private @Nullable ReactiveMongoPersistentEntityIndexCreator indexCreator; private SessionSynchronization sessionSynchronization = SessionSynchronization.ON_ACTUAL_TRANSACTION; @@ -354,6 +358,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati prepareIndexCreator(applicationContext); eventPublisher = applicationContext; + entityCallbacks = new ReactiveEntityCallbacks(applicationContext); if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } @@ -1320,23 +1325,27 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati BeforeConvertEvent event = new BeforeConvertEvent<>(objectToSave, collectionName); T toConvert = maybeEmitEvent(event).getSource(); + return maybeCallBeforeConvert(toConvert, collectionName).flatMap(toSave -> { - AdaptibleEntity entity = operations.forEntity(toConvert, mongoConverter.getConversionService()); - entity.assertUpdateableIdIfNotSet(); + AdaptibleEntity entity = operations.forEntity(toConvert, mongoConverter.getConversionService()); + entity.assertUpdateableIdIfNotSet(); - T initialized = entity.initializeVersionProperty(); - Document dbDoc = entity.toMappedDocument(writer).getDocument(); + T initialized = entity.initializeVersionProperty(); + Document dbDoc = entity.toMappedDocument(writer).getDocument(); - maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName)); + maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName)); + return maybeCallBeforeSave(initialized, dbDoc, collectionName).flatMap(it -> { - Mono afterInsert = insertDocument(collectionName, dbDoc, initialized.getClass()).map(id -> { + Mono afterInsert = insertDocument(collectionName, dbDoc, it.getClass()).map(id -> { - T saved = entity.populateIdIfNecessary(id); - maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); - return saved; + T saved = entity.populateIdIfNecessary(id); + maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); + return saved; + }); + + return afterInsert; + }); }); - - return afterInsert; }); } @@ -1397,20 +1406,23 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(writer, "MongoWriter must not be null!"); Mono, Document>>> prepareDocuments = Flux.fromIterable(batchToSave) - .map(uninitialized -> { + .flatMap(uninitialized -> { BeforeConvertEvent event = new BeforeConvertEvent<>(uninitialized, collectionName); T toConvert = maybeEmitEvent(event).getSource(); - AdaptibleEntity entity = operations.forEntity(toConvert, mongoConverter.getConversionService()); - entity.assertUpdateableIdIfNotSet(); + return maybeCallBeforeConvert(toConvert, collectionName).flatMap(it -> { - T initialized = entity.initializeVersionProperty(); - Document dbDoc = entity.toMappedDocument(writer).getDocument(); + AdaptibleEntity entity = operations.forEntity(it, mongoConverter.getConversionService()); + entity.assertUpdateableIdIfNotSet(); - maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName)); + T initialized = entity.initializeVersionProperty(); + Document dbDoc = entity.toMappedDocument(writer).getDocument(); - return Tuples.of(entity, dbDoc); + maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName)); + + return maybeCallBeforeSave(initialized, dbDoc, collectionName).thenReturn(Tuples.of(entity, dbDoc)); + }); }).collectList(); Flux, Document>> insertDocuments = prepareDocuments.flatMapMany(tuples -> { @@ -1496,17 +1508,21 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati source.assertUpdateableIdIfNotSet(); BeforeConvertEvent event = new BeforeConvertEvent<>(toSave, collectionName); - T afterEvent = ReactiveMongoTemplate.this.maybeEmitEvent(event).getSource(); + T afterEvent = maybeEmitEvent(event).getSource(); - MappedDocument mapped = operations.forEntity(toSave).toMappedDocument(mongoConverter); - Document document = mapped.getDocument(); + return maybeCallBeforeConvert(afterEvent, collectionName).flatMap(toConvert -> { - ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeSaveEvent<>(afterEvent, document, collectionName)); + MappedDocument mapped = operations.forEntity(toSave).toMappedDocument(mongoConverter); + Document document = mapped.getDocument(); - return doUpdate(collectionName, query, mapped.updateWithoutId(), afterEvent.getClass(), false, false) - .map(result -> { - return maybeEmitEvent(new AfterSaveEvent(afterEvent, document, collectionName)).getSource(); + 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(); }); + }); + }); }); } @@ -1518,14 +1534,20 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati T toSave = maybeEmitEvent(new BeforeConvertEvent(objectToSave, collectionName)).getSource(); - AdaptibleEntity entity = operations.forEntity(toSave, mongoConverter.getConversionService()); - Document dbDoc = entity.toMappedDocument(writer).getDocument(); - maybeEmitEvent(new BeforeSaveEvent(toSave, dbDoc, collectionName)); + return maybeCallBeforeConvert(toSave, collectionName).flatMap(toConvert -> { - return saveDocument(collectionName, dbDoc, toSave.getClass()).map(id -> { + AdaptibleEntity entity = operations.forEntity(toConvert, mongoConverter.getConversionService()); + Document dbDoc = entity.toMappedDocument(writer).getDocument(); + maybeEmitEvent(new BeforeSaveEvent(toConvert, dbDoc, collectionName)); - T saved = entity.populateIdIfNecessary(id); - return maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)).getSource(); + return maybeCallBeforeSave(toConvert, dbDoc, collectionName).flatMap(it -> { + + return saveDocument(collectionName, dbDoc, it.getClass()).map(id -> { + + T saved = entity.populateIdIfNecessary(id); + return maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)).getSource(); + }); + }); }); }); } @@ -2494,9 +2516,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati maybeEmitEvent(new BeforeSaveEvent<>(replacement, replacement, collectionName)); - return executeFindOneInternal( - new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(this.mongoConverter, entityType, resultType, collectionName), collectionName); + return maybeCallBeforeSave(replacement, replacement, collectionName).flatMap(it -> { + + return executeFindOneInternal( + new FindAndReplaceCallback(mappedQuery, mappedFields, mappedSort, it, collation, options), + new ProjectingReadCallback<>(this.mongoConverter, entityType, resultType, collectionName), collectionName); + + }); }); } @@ -2509,6 +2535,28 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return event; } + @SuppressWarnings("unchecked") + protected Mono maybeCallBeforeConvert(T object, String collection) { + + if (null != entityCallbacks) { + return entityCallbacks.callbackLater(object, ReactiveBeforeConvertCallback.class, + (cb, t) -> cb.onBeforeConvert(t, collection)); + } + + return Mono.just(object); + } + + @SuppressWarnings("unchecked") + protected Mono maybeCallBeforeSave(T object, Document document, String collection) { + + if (null != entityCallbacks) { + return entityCallbacks.callbackLater(object, ReactiveBeforeSaveCallback.class, + (cb, t) -> cb.onBeforeSave(t, document, collection)); + } + + return Mono.just(object); + } + private MongoCollection getAndPrepareCollection(MongoDatabase db, String collectionName) { try { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallback.java new file mode 100644 index 000000000..30d9ffac5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 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.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; + +/** + * {@link EntityCallback} to populate auditing related fields on an entity about to be saved. + * + * @author Mark Paluch + * @since 2.2 + */ +public class AuditingEntityCallback implements BeforeConvertCallback, Ordered { + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link AuditingEntityCallback} using the given {@link MappingContext} and {@link AuditingHandler} + * provided by the given {@link ObjectFactory}. + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public AuditingEntityCallback(ObjectFactory auditingHandlerFactory) { + + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + this.auditingHandlerFactory = auditingHandlerFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback#onBeforeConvert(java.lang.Object, java.lang.String) + */ + @Override + public Object onBeforeConvert(Object entity, String collection) { + return auditingHandlerFactory.getObject().markAudited(entity); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return 100; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListener.java index 0f7af74cf..85a220a5c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListener.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListener.java @@ -28,7 +28,9 @@ import org.springframework.util.Assert; * * @author Oliver Gierke * @author Thomas Darimont + * @deprecated since 2.2, use {@link AuditingEntityCallback}. */ +@Deprecated public class AuditingEventListener implements ApplicationListener>, Ordered { private final ObjectFactory auditingHandlerFactory; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java new file mode 100644 index 000000000..2f39e1e6f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeConvertCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 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.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.callback.SimpleEntityCallbacks; + +/** + * Callback being invoked before a domain object is converted to be persisted. + * + * @author Mark Paluch + * @since 2.2 + * @see SimpleEntityCallbacks + */ +@FunctionalInterface +public interface BeforeConvertCallback extends EntityCallback { + + /** + * Entity callback method invoked before a domain object is converted to be persisted. Can return either the same of a + * modified instance of the domain object. + * + * @param entity the domain object to save. + * @param collection name of the collection. + * @return the domain object to be persisted. + */ + T onBeforeConvert(T entity, String collection); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java new file mode 100644 index 000000000..e682e89eb --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeSaveCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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; +import org.springframework.data.mapping.callback.SimpleEntityCallbacks; + +/** + * Entity callback triggered before save of a document. + * + * @author Mark Paluch + * @since 2.2 + * @see SimpleEntityCallbacks + */ +@FunctionalInterface +public interface BeforeSaveCallback extends EntityCallback { + + /** + * Entity callback method invoked before a domain object is saved. Can return either the same of a modified instance + * of the domain object and can modify {@link Document} contents. This method called after converting the + * {@code entity} to {@link Document} so effectively the document is used as outcome of invoking this callback. + * + * @param entity the domain object to save. + * @param document {@link Document} representing the {@code entity}. + * @param collection name of the collection. + * @return the domain object to be persisted. + */ + T onBeforeSave(T entity, Document document, String collection); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAuditingEntityCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAuditingEntityCallback.java new file mode 100644 index 000000000..0a4ababd9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveAuditingEntityCallback.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 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 reactor.core.publisher.Mono; + +import org.reactivestreams.Publisher; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; + +/** + * Reactive {@link EntityCallback} to populate auditing related fields on an entity about to be saved. + * + * @author Mark Paluch + * @since 2.2 + */ +public class ReactiveAuditingEntityCallback implements ReactiveBeforeConvertCallback, Ordered { + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link ReactiveAuditingEntityCallback} using the given {@link MappingContext} and + * {@link AuditingHandler} provided by the given {@link ObjectFactory}. + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public ReactiveAuditingEntityCallback(ObjectFactory auditingHandlerFactory) { + + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + this.auditingHandlerFactory = auditingHandlerFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback#onBeforeConvert(java.lang.Object, java.lang.String) + */ + @Override + public Publisher onBeforeConvert(Object entity, String collection) { + return Mono.just(auditingHandlerFactory.getObject().markAudited(entity)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return 100; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeConvertCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeConvertCallback.java new file mode 100644 index 000000000..ba9a086d5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeConvertCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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.reactivestreams.Publisher; + +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; + +/** + * Callback being invoked before a domain object is converted to be persisted. + * + * @author Mark Paluch + * @since 2.2 + * @see ReactiveEntityCallbacks + */ +@FunctionalInterface +public interface ReactiveBeforeConvertCallback extends EntityCallback { + + /** + * Entity callback method invoked before a domain object is converted to be persisted. Can return either the same of a + * modified instance of the domain object. + * + * @param entity the domain object to save. + * @param collection name of the collection. + * @return a {@link Publisher} emitting the domain object to be persisted. + */ + Publisher onBeforeConvert(T entity, String collection); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java new file mode 100644 index 000000000..bb085d6ff --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveBeforeSaveCallback.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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 before save of a document. + * + * @author Mark Paluch + * @since 2.2 + * @see ReactiveEntityCallbacks + */ +@FunctionalInterface +public interface ReactiveBeforeSaveCallback extends EntityCallback { + + /** + * Entity callback method invoked before a domain object is saved. Can return either the same of a modified instance + * of the domain object and can modify {@link Document} contents. This method is called after converting the + * {@code entity} to {@link Document} so effectively the document is used as outcome of invoking this callback. + * + * @param entity the domain object to save. + * @param document {@link Document} representing the {@code entity}. + * @param collection name of the collection. + * @return a {@link Publisher} emitting the domain object to be persisted. + */ + Publisher onBeforeSave(T entity, Document document, String collection); +} diff --git a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas index 473e052a1..27a3500ab 100644 --- a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas +++ b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas @@ -9,4 +9,5 @@ http\://www.springframework.org/schema/data/mongo/spring-mongo-1.8.xsd=org/sprin http\://www.springframework.org/schema/data/mongo/spring-mongo-1.10.xsd=org/springframework/data/mongodb/config/spring-mongo-1.10.xsd http\://www.springframework.org/schema/data/mongo/spring-mongo-1.10.2.xsd=org/springframework/data/mongodb/config/spring-mongo-1.10.2.xsd http\://www.springframework.org/schema/data/mongo/spring-mongo-2.0.xsd=org/springframework/data/mongodb/config/spring-mongo-2.0.xsd -http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-2.0.xsd +http\://www.springframework.org/schema/data/mongo/spring-mongo-2.2.xsd=org/springframework/data/mongodb/config/spring-mongo-2.0.xsd +http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-2.2.xsd diff --git a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd new file mode 100644 index 000000000..3b27d2a2f --- /dev/null +++ b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd @@ -0,0 +1,687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The WriteConcern that will be the default value used when asking the MongoDbFactory for a DB object + + + + + + + + + + + + + + The reference to a MongoTemplate. Will default to 'mongoTemplate'. + + + + + + + Enables creation of indexes for queries that get derived from the method name + and thus reference domain class properties. Defaults to false. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a DbFactory. + + + + + + + + + + + + The reference to a MongoTypeMapper to be used by this MappingMongoConverter. + + + + + + + The reference to a MappingContext. Will default to 'mappingContext'. + + + + + + + Disables JSR-303 validation on MongoDB documents before they are saved. By default it is set to false. + + + + + + + + + + Enables abbreviating the field names for domain class properties to the + first character of their camel case names, e.g. fooBar -> fb. Defaults to false. + + + + + + + + + + The reference to a FieldNamingStrategy. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A reference to a custom converter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a DbFactory. + + + + + + + + + + + + The WriteConcern that will be the default value used when asking the MongoDbFactory for a DB object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a DbFactory. + + + + + + + + + + + + + + + + diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java index f2ea48b4d..b3bc81e24 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AuditingIntegrationTests.java @@ -20,22 +20,25 @@ import static org.junit.Assert.*; import org.joda.time.DateTime; import org.junit.Test; + import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.mapping.callback.SimpleEntityCallbacks; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; /** * Integration test for the auditing support. * * @author Oliver Gierke + * @author Mark Paluch */ public class AuditingIntegrationTests { - @Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883 + @Test // DATAMONGO-577, DATAMONGO-800, DATAMONGO-883, 2261 public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception { AbstractApplicationContext context = new ClassPathXmlApplicationContext("auditing.xml", getClass()); @@ -43,17 +46,18 @@ public class AuditingIntegrationTests { MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); mappingContext.getPersistentEntity(Entity.class); + SimpleEntityCallbacks callbacks = new SimpleEntityCallbacks(context); + Entity entity = new Entity(); - BeforeConvertEvent event = new BeforeConvertEvent(entity, "collection-1"); - context.publishEvent(event); + entity = callbacks.callback(entity, BeforeConvertCallback.class, (cb, e) -> cb.onBeforeConvert(e, "collection-1")); assertThat(entity.created, is(notNullValue())); assertThat(entity.modified, is(entity.created)); Thread.sleep(10); entity.id = 1L; - event = new BeforeConvertEvent(entity, "collection-1"); - context.publishEvent(event); + + entity = callbacks.callback(entity, BeforeConvertCallback.class, (cb, e) -> cb.onBeforeConvert(e, "collection-1")); assertThat(entity.created, is(notNullValue())); assertThat(entity.modified, is(not(entity.created))); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallbackUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallbackUnitTests.java new file mode 100644 index 000000000..5703e95bd --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEntityCallbackUnitTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2019 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 static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Wither; + +import java.util.Arrays; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.core.Ordered; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +/** + * Unit tests for {@link AuditingEntityCallback}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class AuditingEntityCallbackUnitTests { + + IsNewAwareAuditingHandler handler; + AuditingEntityCallback callback; + + @Before + public void setUp() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + mappingContext.getPersistentEntity(Sample.class); + + handler = spy(new IsNewAwareAuditingHandler(new PersistentEntities(Arrays.asList(mappingContext)))); + + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markCreated(any()); + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markModified(any()); + + callback = new AuditingEntityCallback(() -> handler); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-2261 + public void rejectsNullAuditingHandler() { + new AuditingEntityCallback(null); + } + + @Test // DATAMONGO-2261 + public void triggersCreationMarkForObjectWithEmptyId() { + + Sample sample = new Sample(); + callback.onBeforeConvert(sample, "foo"); + + verify(handler, times(1)).markCreated(sample); + verify(handler, times(0)).markModified(any()); + } + + @Test // DATAMONGO-2261 + public void triggersModificationMarkForObjectWithSetId() { + + Sample sample = new Sample(); + sample.id = "id"; + callback.onBeforeConvert(sample, "foo"); + + verify(handler, times(0)).markCreated(any()); + verify(handler, times(1)).markModified(sample); + } + + @Test // DATAMONGO-2261 + public void hasExplicitOrder() { + + assertThat(callback, is(instanceOf(Ordered.class))); + assertThat(callback.getOrder(), is(100)); + } + + @Test // DATAMONGO-2261 + public void propagatesChangedInstanceToEvent() { + + ImmutableSample sample = new ImmutableSample(); + + ImmutableSample newSample = new ImmutableSample(); + IsNewAwareAuditingHandler handler = mock(IsNewAwareAuditingHandler.class); + doReturn(newSample).when(handler).markAudited(eq(sample)); + + AuditingEntityCallback listener = new AuditingEntityCallback(() -> handler); + Object result = listener.onBeforeConvert(sample, "foo"); + + assertThat(result).isSameAs(newSample); + } + + static class Sample { + + @Id String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; + } + + @Value + @Wither + @AllArgsConstructor + @NoArgsConstructor(force = true) + static class ImmutableSample { + + @Id String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; + } +}