Skip to content

DATACMNS-1231 - Add reactive auditing infrastructure #458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.4.0-SNAPSHOT</version>
<version>2.4.0-DATACMNS-1231-SNAPSHOT</version>

<name>Spring Data Core</name>

Expand Down
40 changes: 35 additions & 5 deletions src/main/asciidoc/auditing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
----
Expand Down
127 changes: 7 additions & 120 deletions src/main/java/org/springframework/data/auditing/AuditingHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,11 @@
*/
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;
Expand All @@ -39,16 +34,11 @@
* @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
Expand All @@ -73,9 +63,9 @@ public AuditingHandler(
*/
public AuditingHandler(PersistentEntities entities) {

super(entities);
Assert.notNull(entities, "PersistentEntities must not be null!");

this.factory = new MappingAuditableBeanWrapperFactory(entities);
this.auditorAware = Optional.empty();
}

Expand All @@ -90,36 +80,6 @@ public void setAuditorAware(AuditorAware<?> auditorAware) {
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.
*
Expand All @@ -129,7 +89,7 @@ public <T> T markCreated(T source) {

Assert.notNull(source, "Entity must not be null!");

return touch(source, true);
return markCreated(getAuditor(), source);
}

/**
Expand All @@ -141,86 +101,13 @@ public <T> T markModified(T source) {

Assert.notNull(source, "Entity must not be null!");

return touch(source, false);
return markModified(getAuditor(), source);
}

/**
* 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);
Auditor<?> getAuditor() {

return now;
return auditorAware.map(AuditorAware::getCurrentAuditor).map(Auditor::ofOptional) //
.orElse(Auditor.none());
}

/*
Expand Down
Loading