Browse Source
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: #458pull/460/head
8 changed files with 544 additions and 130 deletions
@ -0,0 +1,203 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -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 @@
@@ -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; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue