DATACMNS-1231 - Add reactive auditing infrastructure.
We now provide a reactive variant for auditing with ReactiveAuditingHandler and ReactiveIsNewAwareAuditingHandler. Extracted common auditing functionality into AuditingHandlerSupport which serves as base class for AuditingHandler and ReactiveAuditingHandler. Original Pull Request: #458
This commit is contained in:
committed by
Christoph Strobl
parent
0c37484b19
commit
d3af4a79e4
@@ -41,19 +41,49 @@ In case you use either `@CreatedBy` or `@LastModifiedBy`, the auditing infrastru
|
||||
|
||||
The following example shows an implementation of the interface that uses Spring Security's `Authentication` object:
|
||||
|
||||
.Implementation of AuditorAware based on Spring Security
|
||||
.Implementation of `AuditorAware` based on Spring Security
|
||||
====
|
||||
[source, java]
|
||||
----
|
||||
class SpringSecurityAuditorAware implements AuditorAware<User> {
|
||||
|
||||
@Override
|
||||
public Optional<User> getCurrentAuditor() {
|
||||
|
||||
return Optional.ofNullable(SecurityContextHolder.getContext())
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.filter(Authentication::isAuthenticated)
|
||||
.map(Authentication::getPrincipal)
|
||||
.map(User.class::cast);
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.filter(Authentication::isAuthenticated)
|
||||
.map(Authentication::getPrincipal)
|
||||
.map(User.class::cast);
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The implementation accesses the `Authentication` object provided by Spring Security and looks up the custom `UserDetails` instance that you have created in your `UserDetailsService` implementation. We assume here that you are exposing the domain user through the `UserDetails` implementation but that, based on the `Authentication` found, you could also look it up from anywhere.
|
||||
|
||||
[[auditing.reactive-auditor-aware]]
|
||||
=== `ReactiveAuditorAware`
|
||||
|
||||
When using reactive infrastructure you might want to make use of contextual information to provide `@CreatedBy` or `@LastModifiedBy` information.
|
||||
We provide an `ReactiveAuditorAware<T>` SPI interface that you have to implement to tell the infrastructure who the current user or system interacting with the application is. The generic type `T` defines what type the properties annotated with `@CreatedBy` or `@LastModifiedBy` have to be.
|
||||
|
||||
The following example shows an implementation of the interface that uses reactive Spring Security's `Authentication` object:
|
||||
|
||||
.Implementation of `ReactiveAuditorAware` based on Spring Security
|
||||
====
|
||||
[source, java]
|
||||
----
|
||||
class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {
|
||||
|
||||
@Override
|
||||
public Mono<User> getCurrentAuditor() {
|
||||
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.filter(Authentication::isAuthenticated)
|
||||
.map(Authentication::getPrincipal)
|
||||
.map(User.class::cast);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -15,16 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.auditing;
|
||||
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.joda.time.DateTime;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.data.domain.Auditable;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
@@ -39,16 +35,11 @@ import org.springframework.util.Assert;
|
||||
* @author Christoph Strobl
|
||||
* @since 1.5
|
||||
*/
|
||||
public class AuditingHandler implements InitializingBean {
|
||||
public class AuditingHandler extends AuditingHandlerSupport implements InitializingBean {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AuditingHandler.class);
|
||||
|
||||
private final DefaultAuditableBeanWrapperFactory factory;
|
||||
|
||||
private DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE;
|
||||
private Optional<AuditorAware<?>> auditorAware;
|
||||
private boolean dateTimeForNow = true;
|
||||
private boolean modifyOnCreation = true;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AuditableBeanWrapper} using the given {@link MappingContext} when looking up auditing metadata
|
||||
@@ -73,9 +64,9 @@ public class AuditingHandler implements InitializingBean {
|
||||
*/
|
||||
public AuditingHandler(PersistentEntities entities) {
|
||||
|
||||
super(entities);
|
||||
Assert.notNull(entities, "PersistentEntities must not be null!");
|
||||
|
||||
this.factory = new MappingAuditableBeanWrapperFactory(entities);
|
||||
this.auditorAware = Optional.empty();
|
||||
}
|
||||
|
||||
@@ -90,36 +81,6 @@ public class AuditingHandler implements InitializingBean {
|
||||
this.auditorAware = Optional.of(auditorAware);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter do determine if {@link Auditable#setCreatedDate(DateTime)} and
|
||||
* {@link Auditable#setLastModifiedDate(DateTime)} shall be filled with the current Java time. Defaults to
|
||||
* {@code true}. One might set this to {@code false} to use database features to set entity time.
|
||||
*
|
||||
* @param dateTimeForNow the dateTimeForNow to set
|
||||
*/
|
||||
public void setDateTimeForNow(boolean dateTimeForNow) {
|
||||
this.dateTimeForNow = dateTimeForNow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true if you want to treat entity creation as modification and thus setting the current date as
|
||||
* modification date during creation, too. Defaults to {@code true}.
|
||||
*
|
||||
* @param modifyOnCreation if modification information shall be set on creation, too
|
||||
*/
|
||||
public void setModifyOnCreation(boolean modifyOnCreation) {
|
||||
this.modifyOnCreation = modifyOnCreation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DateTimeProvider} to be used to determine the dates to be set.
|
||||
*
|
||||
* @param dateTimeProvider
|
||||
*/
|
||||
public void setDateTimeProvider(DateTimeProvider dateTimeProvider) {
|
||||
this.dateTimeProvider = dateTimeProvider == null ? CurrentDateTimeProvider.INSTANCE : dateTimeProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given object as created.
|
||||
*
|
||||
@@ -129,7 +90,7 @@ public class AuditingHandler implements InitializingBean {
|
||||
|
||||
Assert.notNull(source, "Entity must not be null!");
|
||||
|
||||
return touch(source, true);
|
||||
return markCreated(auditorAware.flatMap(AuditorAware::getCurrentAuditor).orElse(null), source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,86 +102,7 @@ public class AuditingHandler implements InitializingBean {
|
||||
|
||||
Assert.notNull(source, "Entity must not be null!");
|
||||
|
||||
return touch(source, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given source is considered to be auditable in the first place
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected final boolean isAuditable(Object source) {
|
||||
|
||||
Assert.notNull(source, "Source must not be null!");
|
||||
|
||||
return factory.getBeanWrapperFor(source).isPresent();
|
||||
}
|
||||
|
||||
private <T> T touch(T target, boolean isNew) {
|
||||
|
||||
Optional<AuditableBeanWrapper<T>> wrapper = factory.getBeanWrapperFor(target);
|
||||
|
||||
return wrapper.map(it -> {
|
||||
|
||||
Optional<Object> auditor = touchAuditor(it, isNew);
|
||||
Optional<TemporalAccessor> now = dateTimeForNow ? touchDate(it, isNew) : Optional.empty();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
Object defaultedNow = now.map(Object::toString).orElse("not set");
|
||||
Object defaultedAuditor = auditor.map(Object::toString).orElse("unknown");
|
||||
|
||||
logger.debug(LogMessage.format("Touched %s - Last modification at %s by %s", target, defaultedNow, defaultedAuditor));
|
||||
}
|
||||
|
||||
return it.getBean();
|
||||
|
||||
}).orElse(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets modifying and creating auditor. Creating auditor is only set on new auditables.
|
||||
*
|
||||
* @param auditable
|
||||
* @return
|
||||
*/
|
||||
private Optional<Object> touchAuditor(AuditableBeanWrapper<?> wrapper, boolean isNew) {
|
||||
|
||||
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
|
||||
|
||||
return auditorAware.map(it -> {
|
||||
|
||||
Optional<?> auditor = it.getCurrentAuditor();
|
||||
|
||||
Assert.notNull(auditor,
|
||||
() -> String.format("Auditor must not be null! Returned by: %s!", AopUtils.getTargetClass(it)));
|
||||
|
||||
auditor.filter(__ -> isNew).ifPresent(wrapper::setCreatedBy);
|
||||
auditor.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedBy);
|
||||
|
||||
return auditor;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Touches the auditable regarding modification and creation date. Creation date is only set on new auditables.
|
||||
*
|
||||
* @param wrapper
|
||||
* @return
|
||||
*/
|
||||
private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
|
||||
|
||||
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
|
||||
|
||||
Optional<TemporalAccessor> now = dateTimeProvider.getNow();
|
||||
|
||||
Assert.notNull(now, () -> String.format("Now must not be null! Returned by: %s!", dateTimeProvider.getClass()));
|
||||
|
||||
now.filter(__ -> isNew).ifPresent(wrapper::setCreatedDate);
|
||||
now.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedDate);
|
||||
|
||||
return now;
|
||||
return markModified(auditorAware.flatMap(AuditorAware::getCurrentAuditor).orElse(null), source);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -229,6 +111,8 @@ public class AuditingHandler implements InitializingBean {
|
||||
*/
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
super.afterPropertiesSet();
|
||||
|
||||
if (!auditorAware.isPresent()) {
|
||||
logger.debug("No AuditorAware set! Auditing will not be applied!");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2012-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.auditing;
|
||||
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.data.domain.Auditable;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Support class to implement auditing handlers.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 2.4
|
||||
*/
|
||||
public abstract class AuditingHandlerSupport implements InitializingBean {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AuditingHandlerSupport.class);
|
||||
|
||||
private final DefaultAuditableBeanWrapperFactory factory;
|
||||
|
||||
private DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE;
|
||||
private boolean dateTimeForNow = true;
|
||||
private boolean modifyOnCreation = true;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AuditableBeanWrapper} using the given {@link PersistentEntities} when looking up auditing
|
||||
* metadata via reflection.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
*/
|
||||
public AuditingHandlerSupport(PersistentEntities entities) {
|
||||
|
||||
Assert.notNull(entities, "PersistentEntities must not be null!");
|
||||
|
||||
this.factory = new MappingAuditableBeanWrapperFactory(entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter do determine if {@link Auditable#setCreatedDate(TemporalAccessor)}} and
|
||||
* {@link Auditable#setLastModifiedDate(TemporalAccessor)} shall be filled with the current Java time. Defaults to
|
||||
* {@code true}. One might set this to {@code false} to use database features to set entity time.
|
||||
*
|
||||
* @param dateTimeForNow the dateTimeForNow to set
|
||||
*/
|
||||
public void setDateTimeForNow(boolean dateTimeForNow) {
|
||||
this.dateTimeForNow = dateTimeForNow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true if you want to treat entity creation as modification and thus setting the current date as
|
||||
* modification date during creation, too. Defaults to {@code true}.
|
||||
*
|
||||
* @param modifyOnCreation if modification information shall be set on creation, too
|
||||
*/
|
||||
public void setModifyOnCreation(boolean modifyOnCreation) {
|
||||
this.modifyOnCreation = modifyOnCreation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DateTimeProvider} to be used to determine the dates to be set.
|
||||
*
|
||||
* @param dateTimeProvider
|
||||
*/
|
||||
public void setDateTimeProvider(@Nullable DateTimeProvider dateTimeProvider) {
|
||||
this.dateTimeProvider = dateTimeProvider == null ? CurrentDateTimeProvider.INSTANCE : dateTimeProvider;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
public void afterPropertiesSet() {}
|
||||
|
||||
/**
|
||||
* Returns whether the given source is considered to be auditable in the first place
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected final boolean isAuditable(Object source) {
|
||||
|
||||
Assert.notNull(source, "Source must not be null!");
|
||||
|
||||
return factory.getBeanWrapperFor(source).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given object as created.
|
||||
*
|
||||
* @param auditor
|
||||
* @param source
|
||||
*/
|
||||
<T> T markCreated(@Nullable Object auditor, T source) {
|
||||
|
||||
Assert.notNull(source, "Entity must not be null!");
|
||||
|
||||
return touch(auditor, source, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given object as modified.
|
||||
*
|
||||
* @param auditor
|
||||
* @param source
|
||||
*/
|
||||
<T> T markModified(@Nullable Object auditor, T source) {
|
||||
|
||||
Assert.notNull(source, "Entity must not be null!");
|
||||
|
||||
return touch(auditor, source, false);
|
||||
}
|
||||
|
||||
private <T> T touch(@Nullable Object auditor, T target, boolean isNew) {
|
||||
|
||||
Optional<AuditableBeanWrapper<T>> wrapper = factory.getBeanWrapperFor(target);
|
||||
|
||||
return wrapper.map(it -> {
|
||||
|
||||
if (auditor != null) {
|
||||
touchAuditor(auditor, it, isNew);
|
||||
}
|
||||
Optional<TemporalAccessor> now = dateTimeForNow ? touchDate(it, isNew) : Optional.empty();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
Object defaultedNow = now.map(Object::toString).orElse("not set");
|
||||
Object defaultedAuditor = auditor != null ? auditor.toString() : "unknown";
|
||||
|
||||
logger.debug(
|
||||
LogMessage.format("Touched %s - Last modification at %s by %s", target, defaultedNow, defaultedAuditor));
|
||||
}
|
||||
|
||||
return it.getBean();
|
||||
}).orElse(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets modifying and creating auditor. Creating auditor is only set on new auditables.
|
||||
*
|
||||
* @param auditor
|
||||
* @param wrapper
|
||||
* @param isNew
|
||||
* @return
|
||||
*/
|
||||
private void touchAuditor(Object auditor, AuditableBeanWrapper<?> wrapper, boolean isNew) {
|
||||
|
||||
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
|
||||
Assert.notNull(auditor, "Auditor must not be null!");
|
||||
|
||||
if (isNew) {
|
||||
wrapper.setCreatedBy(auditor);
|
||||
}
|
||||
|
||||
if (!isNew || modifyOnCreation) {
|
||||
wrapper.setLastModifiedBy(auditor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Touches the auditable regarding modification and creation date. Creation date is only set on new auditables.
|
||||
*
|
||||
* @param wrapper
|
||||
* @param isNew
|
||||
* @return
|
||||
*/
|
||||
private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
|
||||
|
||||
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
|
||||
|
||||
Optional<TemporalAccessor> now = dateTimeProvider.getNow();
|
||||
|
||||
Assert.notNull(now, () -> String.format("Now must not be null! Returned by: %s!", dateTimeProvider.getClass()));
|
||||
|
||||
now.filter(__ -> isNew).ifPresent(wrapper::setCreatedDate);
|
||||
now.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedDate);
|
||||
|
||||
return now;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.auditing;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.ReactiveAuditorAware;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Auditing handler to mark entity objects created and modified.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.4
|
||||
*/
|
||||
public class ReactiveAuditingHandler extends AuditingHandlerSupport {
|
||||
|
||||
private ReactiveAuditorAware<?> auditorAware = Mono::empty;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AuditableBeanWrapper} using the given {@link PersistentEntities} when looking up auditing
|
||||
* metadata via reflection.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
*/
|
||||
public ReactiveAuditingHandler(PersistentEntities entities) {
|
||||
super(entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter to inject a {@link ReactiveAuditorAware} component to retrieve the current auditor.
|
||||
*
|
||||
* @param auditorAware must not be {@literal null}.
|
||||
*/
|
||||
public void setAuditorAware(ReactiveAuditorAware<?> auditorAware) {
|
||||
|
||||
Assert.notNull(auditorAware, "AuditorAware must not be null!");
|
||||
this.auditorAware = auditorAware;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given object as created.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
*/
|
||||
public <T> Mono<T> markCreated(T source) {
|
||||
|
||||
Assert.notNull(source, "Entity must not be null!");
|
||||
|
||||
return auditorAware.getCurrentAuditor().map(Optional::of) //
|
||||
.defaultIfEmpty(Optional.empty()) //
|
||||
.map(auditor -> markCreated(auditor.orElse(null), source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given object as modified.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
*/
|
||||
public <T> Mono<T> markModified(T source) {
|
||||
|
||||
Assert.notNull(source, "Entity must not be null!");
|
||||
|
||||
return auditorAware.getCurrentAuditor().map(Optional::of) //
|
||||
.defaultIfEmpty(Optional.empty()) //
|
||||
.map(auditor -> markModified(auditor.orElse(null), source));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.auditing;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.support.IsNewStrategy;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link AuditingHandler} extension that uses {@link PersistentEntity#isNew(Object)} to expose a generic
|
||||
* {@link #markAudited(Object)} method that will route calls to {@link #markCreated(Object)} or
|
||||
* {@link #markModified(Object)} based on the {@link IsNewStrategy} determined from the factory.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.4
|
||||
*/
|
||||
public class ReactiveIsNewAwareAuditingHandler extends ReactiveAuditingHandler {
|
||||
|
||||
private final PersistentEntities entities;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ReactiveIsNewAwareAuditingHandler} for the given {@link MappingContext}.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
*/
|
||||
public ReactiveIsNewAwareAuditingHandler(PersistentEntities entities) {
|
||||
|
||||
super(entities);
|
||||
|
||||
this.entities = entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given object created or modified based on {@link PersistentEntity#isNew(Object)}. Will route the calls to
|
||||
* {@link #markCreated(Object)} and {@link #markModified(Object)} accordingly.
|
||||
*
|
||||
* @param object must not be {@literal null}.
|
||||
*/
|
||||
public Mono<Object> markAudited(Object object) {
|
||||
|
||||
Assert.notNull(object, "Source object must not be null!");
|
||||
|
||||
if (!isAuditable(object)) {
|
||||
return Mono.just(object);
|
||||
}
|
||||
|
||||
PersistentEntity<?, ? extends PersistentProperty<?>> entity = entities
|
||||
.getRequiredPersistentEntity(object.getClass());
|
||||
|
||||
return entity.isNew(object) ? markCreated(object) : markModified(object);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import java.util.Optional;
|
||||
/**
|
||||
* Interface for components that are aware of the application's current auditor. This will be some kind of user mostly.
|
||||
*
|
||||
* @param <T> the type of the auditing instance
|
||||
* @param <T> the type of the auditing instance.
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface AuditorAware<T> {
|
||||
@@ -28,7 +28,7 @@ public interface AuditorAware<T> {
|
||||
/**
|
||||
* Returns the current auditor of the application.
|
||||
*
|
||||
* @return the current auditor
|
||||
* @return the current auditor.
|
||||
*/
|
||||
Optional<T> getCurrentAuditor();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2008-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.domain;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Interface for components that are aware of the application's current auditor. This will be some kind of user mostly.
|
||||
*
|
||||
* @param <T> the type of the auditing instance.
|
||||
* @author Mark Paluch
|
||||
* @since 2.4
|
||||
*/
|
||||
public interface ReactiveAuditorAware<T> {
|
||||
|
||||
/**
|
||||
* Returns the current auditor of the application.
|
||||
*
|
||||
* @return the current auditor. If the mono does not emit a value, the auditor is considered to be unknown.
|
||||
*/
|
||||
Mono<T> getCurrentAuditor();
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2008-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.auditing;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import lombok.Value;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.domain.ReactiveAuditorAware;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.mapping.context.SampleMappingContext;
|
||||
|
||||
/**
|
||||
* Unit test for {@link ReactiveAuditingHandler}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
class ReactiveAuditingHandlerUnitTests {
|
||||
|
||||
ReactiveAuditingHandler handler;
|
||||
ReactiveAuditorAware<AuditedUser> auditorAware;
|
||||
|
||||
AuditedUser user;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
SampleMappingContext sampleMappingContext = new SampleMappingContext();
|
||||
sampleMappingContext.getRequiredPersistentEntity(Immutable.class); // initialize to ensure we're using mapping
|
||||
// metadata instead of plain reflection
|
||||
|
||||
handler = new ReactiveAuditingHandler(PersistentEntities.of(sampleMappingContext));
|
||||
user = new AuditedUser();
|
||||
|
||||
auditorAware = mock(ReactiveAuditorAware.class);
|
||||
when(auditorAware.getCurrentAuditor()).thenReturn(Mono.just(user));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markCreatedShouldSetDatesIfAuditorNotSet() {
|
||||
|
||||
Immutable immutable = new Immutable(null, null, null, null);
|
||||
|
||||
handler.markCreated(immutable).as(StepVerifier::create).consumeNextWith(actual -> {
|
||||
|
||||
assertThat(actual.getCreatedDate()).isNotNull();
|
||||
assertThat(actual.getModifiedDate()).isNotNull();
|
||||
|
||||
assertThat(actual.getCreatedBy()).isNull();
|
||||
assertThat(actual.getModifiedBy()).isNull();
|
||||
}).verifyComplete();
|
||||
|
||||
assertThat(immutable.getCreatedDate()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void markModifiedSetsModifiedFields() {
|
||||
|
||||
AuditedUser audited = new AuditedUser();
|
||||
audited.id = 1L;
|
||||
|
||||
handler.setAuditorAware(auditorAware);
|
||||
handler.markModified(audited).as(StepVerifier::create).expectNext(audited).verifyComplete();
|
||||
|
||||
assertThat(audited.getCreatedBy()).isNotPresent();
|
||||
assertThat(audited.getCreatedDate()).isNotPresent();
|
||||
|
||||
assertThat(audited.getLastModifiedBy()).isPresent();
|
||||
assertThat(audited.getLastModifiedDate()).isPresent();
|
||||
|
||||
verify(auditorAware).getCurrentAuditor();
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Immutable {
|
||||
|
||||
@CreatedDate Instant createdDate;
|
||||
@CreatedBy String createdBy;
|
||||
@LastModifiedDate Instant modifiedDate;
|
||||
@LastModifiedBy String modifiedBy;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user