diff --git a/pom.xml b/pom.xml index 3fdbfcb98a..65976fbb1f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,13 @@ - + 4.0.0 org.springframework.data spring-data-commons - 2.4.0-SNAPSHOT + 2.4.0-DATACMNS-1231-SNAPSHOT Spring Data Core diff --git a/src/main/asciidoc/auditing.adoc b/src/main/asciidoc/auditing.adoc index fd5fc8f455..ba571e3e86 100644 --- a/src/main/asciidoc/auditing.adoc +++ b/src/main/asciidoc/auditing.adoc @@ -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 { + @Override public Optional 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` 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 { + + @Override + public Mono getCurrentAuditor() { + + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(User.class::cast); } } ---- diff --git a/src/main/java/org/springframework/data/auditing/AuditingHandler.java b/src/main/java/org/springframework/data/auditing/AuditingHandler.java index 11c5f65ecf..106aaeeb60 100644 --- a/src/main/java/org/springframework/data/auditing/AuditingHandler.java +++ b/src/main/java/org/springframework/data/auditing/AuditingHandler.java @@ -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; @@ -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; - 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 +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(); } @@ -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. * @@ -129,7 +89,7 @@ public T markCreated(T source) { Assert.notNull(source, "Entity must not be null!"); - return touch(source, true); + return markCreated(getAuditor(), source); } /** @@ -141,86 +101,13 @@ public 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 touch(T target, boolean isNew) { - - Optional> wrapper = factory.getBeanWrapperFor(target); - - return wrapper.map(it -> { - - Optional auditor = touchAuditor(it, isNew); - Optional 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 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 touchDate(AuditableBeanWrapper wrapper, boolean isNew) { - - Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!"); - - Optional 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()); } /* diff --git a/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java b/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java new file mode 100644 index 0000000000..bcc1bfc444 --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java @@ -0,0 +1,197 @@ +/* + * 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.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 { + + private static final Log logger = LogFactory.getLog(AuditingHandlerSupport.class); + + private final AuditableBeanWrapperFactory 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 can be {@literal null}, defaults to {@link CurrentDateTimeProvider} in that case. + */ + public void setDateTimeProvider(@Nullable DateTimeProvider dateTimeProvider) { + this.dateTimeProvider = dateTimeProvider == null ? CurrentDateTimeProvider.INSTANCE : dateTimeProvider; + } + + /** + * Returns whether the given source is considered to be auditable in the first place. + * + * @param source must not be {@literal null}. + * @return {@literal true} if the given {@literal source} considered to be auditable. + */ + protected final boolean isAuditable(Object source) { + + Assert.notNull(source, "Source entity must not be null!"); + + return factory.getBeanWrapperFor(source).isPresent(); + } + + /** + * Marks the given object as created. + * + * @param auditor can be {@literal null}. + * @param source must not be {@literal null}. + */ + T markCreated(Auditor auditor, T source) { + + Assert.notNull(source, "Source entity must not be null!"); + + return touch(auditor, source, true); + } + + /** + * Marks the given object as modified. + * + * @param auditor + * @param source + */ + T markModified(Auditor auditor, T source) { + + Assert.notNull(source, "Source entity must not be null!"); + + return touch(auditor, source, false); + } + + private T touch(Auditor auditor, T target, boolean isNew) { + + Optional> wrapper = factory.getBeanWrapperFor(target); + + return wrapper.map(it -> { + + touchAuditor(auditor, it, isNew); + Optional now = dateTimeForNow ? touchDate(it, isNew) : Optional.empty(); + + if (logger.isDebugEnabled()) { + + Object defaultedNow = now.map(Object::toString).orElse("not set"); + Object defaultedAuditor = auditor.isPresent() ? 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(Auditor auditor, AuditableBeanWrapper wrapper, boolean isNew) { + + if(!auditor.isPresent()) { + return; + } + + Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!"); + + if (isNew) { + wrapper.setCreatedBy(auditor.getValue()); + } + + if (!isNew || modifyOnCreation) { + wrapper.setLastModifiedBy(auditor.getValue()); + } + } + + /** + * Touches the auditable regarding modification and creation date. Creation date is only set on new auditables. + * + * @param wrapper + * @param isNew + * @return + */ + private Optional touchDate(AuditableBeanWrapper wrapper, boolean isNew) { + + Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!"); + + Optional 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; + } +} diff --git a/src/main/java/org/springframework/data/auditing/Auditor.java b/src/main/java/org/springframework/data/auditing/Auditor.java new file mode 100644 index 0000000000..67668d6bee --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/Auditor.java @@ -0,0 +1,122 @@ +/* + * 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 java.util.Optional; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Value Object encapsulating the actual auditor value. + * + * @author Christoph Strobl + * @since 2.4 + */ +class Auditor { + + private static final Auditor NONE = new Auditor(null) { + + @Override + public boolean isPresent() { + return false; + } + }; + + private final @Nullable T value; + + private Auditor(@Nullable T value) { + this.value = value; + } + + /** + * @return + */ + @Nullable + public T getValue() { + return value; + } + + /** + * Create an {@link Auditor} for the given {@literal source} value.
+ * If the given {@literal source} is {@literal null} {@link Auditor#none()} is returned. A source that already is an + * {@link Auditor} gets returned as is. + * + * @param source can be {@literal null}. + * @param + * @return {@link Auditor#none()} if the given {@literal source} is {@literal null}. } + */ + public static Auditor of(@Nullable T source) { + + if (source instanceof Auditor) { + return (Auditor) source; + } + + return source == null ? Auditor.none() : new Auditor<>(source); + } + + /** + * Create an {@link Auditor} for the given {@link Optional} value.
+ * If the given {@literal source} is {@link Optional#empty()} {@link Auditor#none()} is returned. An {@link Optional} + * wrapping and {@link Auditor} returns the unwrapped {@link Auditor} instance as is. + * + * @param source must not be {@literal null}. + * @param + * @return {@link Auditor#none()} if the given {@literal source} is {@literal null}. } + */ + public static Auditor ofOptional(@Nullable Optional source) { + return Auditor.of(source.orElse(null)); + } + + /** + * Return an {@link Auditor} that is not present. + * + * @param + * @return never {@literal null}. + */ + public static Auditor none() { + return NONE; + } + + /** + * @return {@literal true} if {@link #getValue()} returns a non {@literal null} value. + */ + public boolean isPresent() { + return getValue() != null; + } + + @Override + public String toString() { + return value != null ? value.toString() : "Auditor.none()"; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Auditor auditor = (Auditor) o; + + return ObjectUtils.nullSafeEquals(value, auditor.value); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(value); + } +} diff --git a/src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java b/src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java new file mode 100644 index 0000000000..36e6ae67fe --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java @@ -0,0 +1,88 @@ +/* + * 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.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 + * @author Christoph Strobl + * @since 2.4 + */ +public class ReactiveAuditingHandler extends AuditingHandlerSupport { + + private ReactiveAuditorAware auditorAware = Mono::empty; + + /** + * Creates a new {@link ReactiveAuditingHandler} using the given {@link PersistentEntities} when looking up auditing + * metadata. + * + * @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 Mono markCreated(T source) { + + Assert.notNull(source, "Entity must not be null!"); + + return getAuditor() // + .map(auditor -> markCreated(auditor, source)); + } + + /** + * Marks the given object as modified. + * + * @param source must not be {@literal null}. + */ + public Mono markModified(T source) { + + Assert.notNull(source, "Entity must not be null!"); + + return getAuditor() // + .map(auditor -> markModified(auditor, source)); + } + + private Mono> getAuditor() { + + return auditorAware.getCurrentAuditor() // + .map(Auditor::of) // + .defaultIfEmpty(Auditor.none()); + } +} diff --git a/src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java b/src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java new file mode 100644 index 0000000000..18e950e44b --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java @@ -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 markAudited(Object object) { + + Assert.notNull(object, "Source object must not be null!"); + + if (!isAuditable(object)) { + return Mono.just(object); + } + + PersistentEntity> entity = entities + .getRequiredPersistentEntity(object.getClass()); + + return entity.isNew(object) ? markCreated(object) : markModified(object); + } +} diff --git a/src/main/java/org/springframework/data/domain/AuditorAware.java b/src/main/java/org/springframework/data/domain/AuditorAware.java index 8da971d956..03b5d8c15c 100644 --- a/src/main/java/org/springframework/data/domain/AuditorAware.java +++ b/src/main/java/org/springframework/data/domain/AuditorAware.java @@ -20,7 +20,7 @@ /** * Interface for components that are aware of the application's current auditor. This will be some kind of user mostly. * - * @param the type of the auditing instance + * @param the type of the auditing instance. * @author Oliver Gierke */ public interface AuditorAware { @@ -28,7 +28,8 @@ public interface AuditorAware { /** * Returns the current auditor of the application. * - * @return the current auditor + * @return the current auditor. */ Optional getCurrentAuditor(); + } diff --git a/src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java b/src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java new file mode 100644 index 0000000000..91dce857db --- /dev/null +++ b/src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java @@ -0,0 +1,36 @@ +/* + * 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.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 the type of the auditing instance. + * @author Mark Paluch + * @since 2.4 + */ +public interface ReactiveAuditorAware { + + /** + * Returns a {@link Mono} publishing the current auditor of the application. + * + * @return the {@link Mono} emitting the current auditor, or an empty one, if the auditor is considered to be unknown. + */ + Mono getCurrentAuditor(); + +} diff --git a/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java b/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java index 20119d5aaf..271bcbae60 100755 --- a/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java +++ b/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java @@ -36,6 +36,7 @@ * Unit test for {@code AuditingHandler}. * * @author Oliver Gierke + * @author Christoph Strobl * @since 1.5 */ @SuppressWarnings("unchecked") @@ -177,6 +178,27 @@ void setsAuditingInfoOnEntityUsingInheritance() { assertThat(result.modified).isNotNull(); } + @Test // DATACMNS-1231 + void getAuditorGetsAuditorNoneWhenNoAuditorAwareNotPresent() { + assertThat(handler.getAuditor()).isEqualTo(Auditor.none()); + } + + @Test // DATACMNS-1231 + void getAuditorGetsAuditorWhenPresent() { + + handler.setAuditorAware(auditorAware); + assertThat(handler.getAuditor()).isEqualTo(Auditor.of(user)); + } + + @Test // DATACMNS-1231 + void getAuditorShouldReturnNoneIfAuditorAwareDoesNotHoldObject() { + + when(auditorAware.getCurrentAuditor()).thenReturn(Optional.empty()); + + handler.setAuditorAware(auditorAware); + assertThat(handler.getAuditor()).isEqualTo(Auditor.none()); + } + static abstract class AbstractModel { @CreatedDate Instant created; diff --git a/src/test/java/org/springframework/data/auditing/AuditorUnitTests.java b/src/test/java/org/springframework/data/auditing/AuditorUnitTests.java new file mode 100644 index 0000000000..316e4de387 --- /dev/null +++ b/src/test/java/org/springframework/data/auditing/AuditorUnitTests.java @@ -0,0 +1,50 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +/** + * @author Christoph Strobl + */ +class AuditorUnitTests { + + @Test // DATACMNS-1231 + void auditorOfEmptyOptionalIsNone() { + assertThat(Auditor.ofOptional(Optional.empty())).isEqualTo(Auditor.none()); + } + + @Test // DATACMNS-1231 + void auditorOfOptionalIsValue() { + assertThat(Auditor.ofOptional(Optional.of("batman"))).isEqualTo(Auditor.of("batman")); + } + + @Test // DATACMNS-1231 + void auditorOfMustNotWrapOtherAuditor() { + + Auditor source = Auditor.of("batman"); + assertThat(Auditor.of(source)).isSameAs(source); + } + + @Test // DATACMNS-1231 + void auditorOfNullIsNone() { + assertThat(Auditor.of(null)).isEqualTo(Auditor.none()); + } +} diff --git a/src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java b/src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java new file mode 100755 index 0000000000..f84080eabe --- /dev/null +++ b/src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java @@ -0,0 +1,107 @@ +/* + * 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 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 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 // DATACMNS-1231 + 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 // DATACMNS-1231 + 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; + } +}