From f6105beab13ad540b8f3d505cd0b9218d1206ca7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 23 Jul 2020 11:11:03 +0200 Subject: [PATCH] #281 - Add support for auditing. We now provide auditing support that can be enabled through EnableR2dbcAuditing. @Configuration @EnableR2dbcAuditing class Config { @Bean public ReactiveAuditorAware myAuditorProvider() { return new AuditorAwareImpl(); } } --- src/main/asciidoc/index.adoc | 6 +- src/main/asciidoc/new-features.adoc | 1 + .../asciidoc/reference/r2dbc-auditing.adoc | 23 +++++ .../reference/r2dbc-entity-callbacks.adoc | 7 +- .../r2dbc/config/EnableR2dbcAuditing.java | 70 ++++++++++++++ .../config/PersistentEntitiesFactoryBean.java | 59 ++++++++++++ .../r2dbc/config/R2dbcAuditingRegistrar.java | 96 +++++++++++++++++++ .../event/ReactiveAuditingEntityCallback.java | 68 +++++++++++++ .../data/r2dbc/config/AuditingUnitTests.java | 96 +++++++++++++++++++ .../R2dbcAuditingRegistrarUnitTests.java | 50 ++++++++++ 10 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 src/main/asciidoc/reference/r2dbc-auditing.adoc create mode 100644 src/main/java/org/springframework/data/r2dbc/config/EnableR2dbcAuditing.java create mode 100644 src/main/java/org/springframework/data/r2dbc/config/PersistentEntitiesFactoryBean.java create mode 100644 src/main/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrar.java create mode 100644 src/main/java/org/springframework/data/r2dbc/mapping/event/ReactiveAuditingEntityCallback.java create mode 100644 src/test/java/org/springframework/data/r2dbc/config/AuditingUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrarUnitTests.java diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 6b5b38d5..18e5f7e9 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -8,7 +8,7 @@ ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1 :spring-framework-ref: https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference :reactiveStreamsJavadoc: https://www.reactive-streams.org/reactive-streams-{reactiveStreamsVersion}-javadoc -(C) 2018-2019 The original authors. +(C) 2018-2020 The original authors. NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. @@ -34,6 +34,10 @@ include::reference/r2dbc.adoc[leveloffset=+1] include::reference/r2dbc-repositories.adoc[leveloffset=+1] +include::{spring-data-commons-docs}/auditing.adoc[leveloffset=+1] + +include::reference/r2dbc-auditing.adoc[leveloffset=+1] + include::reference/r2dbc-connections.adoc[leveloffset=+1] include::reference/r2dbc-initialization.adoc[leveloffset=+1] diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index a599f947..d4bb2a13 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -5,6 +5,7 @@ == What's New in Spring Data R2DBC 1.2.0 * Support for <>. +* <> through `@EnableR2dbcAuditing`. [[new-features.1-1-0]] == What's New in Spring Data R2DBC 1.1.0 diff --git a/src/main/asciidoc/reference/r2dbc-auditing.adoc b/src/main/asciidoc/reference/r2dbc-auditing.adoc new file mode 100644 index 00000000..818ec2c4 --- /dev/null +++ b/src/main/asciidoc/reference/r2dbc-auditing.adoc @@ -0,0 +1,23 @@ +[[r2dbc.auditing]] +== General Auditing Configuration for R2DBC + +Since Spring Data R2DBC 1.2, auditing can be enabled by annotating a configuration class with the `@EnableR2dbcAuditing` annotation, as the following example shows: + +.Activating auditing using JavaConfig +==== +[source,java] +---- +@Configuration +@EnableR2dbcAuditing +class Config { + + @Bean + public ReactiveAuditorAware myAuditorProvider() { + return new AuditorAwareImpl(); + } +} +---- +==== + +If you expose a bean of type `ReactiveAuditorAware` to the `ApplicationContext`, the auditing infrastructure picks it up automatically and uses it to determine the current user to be set on domain types. +If you have multiple implementations registered in the `ApplicationContext`, you can select the one to be used by explicitly setting the `auditorAwareRef` attribute of `@EnableR2dbcAuditing`. diff --git a/src/main/asciidoc/reference/r2dbc-entity-callbacks.adoc b/src/main/asciidoc/reference/r2dbc-entity-callbacks.adoc index d0cac8b3..67379a23 100644 --- a/src/main/asciidoc/reference/r2dbc-entity-callbacks.adoc +++ b/src/main/asciidoc/reference/r2dbc-entity-callbacks.adoc @@ -1,7 +1,7 @@ [[r2dbc.entity-callbacks]] = Store specific EntityCallbacks -Spring Data R2DBC uses the `EntityCallback` API and reacts on the following callbacks. +Spring Data R2DBC uses the `EntityCallback` API for its auditing support and reacts on the following callbacks. .Supported Entity Callbacks [%header,cols="4"] @@ -22,6 +22,11 @@ Spring Data R2DBC uses the `EntityCallback` API and reacts on the following call Can modify the domain object after reading it from a row. | `Ordered.LOWEST_PRECEDENCE` +| AuditingEntityCallback +| `onBeforeConvert(T entity, SqlIdentifier table)` +| Marks an auditable entity _created_ or _modified_ +| 100 + | BeforeSaveCallback | `onBeforeSave(T entity, OutboundRow row, SqlIdentifier table)` | Invoked before a domain object is saved. + diff --git a/src/main/java/org/springframework/data/r2dbc/config/EnableR2dbcAuditing.java b/src/main/java/org/springframework/data/r2dbc/config/EnableR2dbcAuditing.java new file mode 100644 index 00000000..0ee1acb3 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/config/EnableR2dbcAuditing.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.r2dbc.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.ReactiveAuditorAware; + +/** + * Annotation to enable auditing in R2DBC via annotation configuration. + * + * @author Mark Paluch + * @since 1.2 + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(R2dbcAuditingRegistrar.class) +public @interface EnableR2dbcAuditing { + + /** + * Configures the {@link ReactiveAuditorAware} bean to be used to lookup the current principal. + * + * @return empty {@link String} by default. + */ + String auditorAwareRef() default ""; + + /** + * Configures whether the creation and modification dates are set. Defaults to {@literal true}. + * + * @return {@literal true} by default. + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. + * + * @return {@literal true} by default. + */ + boolean modifyOnCreate() default true; + + /** + * Configures a {@link DateTimeProvider} bean name that allows customizing the timestamp to be used for setting + * creation and modification dates. + * + * @return empty {@link String} by default. + */ + String dateTimeProviderRef() default ""; +} diff --git a/src/main/java/org/springframework/data/r2dbc/config/PersistentEntitiesFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/config/PersistentEntitiesFactoryBean.java new file mode 100644 index 00000000..9c323859 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/config/PersistentEntitiesFactoryBean.java @@ -0,0 +1,59 @@ +/* + * 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.r2dbc.config; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.r2dbc.mapping.R2dbcMappingContext; + +/** + * Simple helper to be able to wire the {@link PersistentEntities} from a {@link R2dbcMappingContext} bean available in + * the application context. + * + * @author Mark Paluch + * @since 1.2 + */ +class PersistentEntitiesFactoryBean implements FactoryBean { + + private final R2dbcMappingContext mappingContext; + + /** + * Creates a new {@link PersistentEntitiesFactoryBean} for the given {@link R2dbcMappingContext}. + * + * @param converter must not be {@literal null}. + */ + public PersistentEntitiesFactoryBean(R2dbcMappingContext mappingContext) { + this.mappingContext = mappingContext; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + @Override + public PersistentEntities getObject() { + return PersistentEntities.of(mappingContext); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + @Override + public Class getObjectType() { + return PersistentEntities.class; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrar.java b/src/main/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrar.java new file mode 100644 index 00000000..7a1448fe --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrar.java @@ -0,0 +1,96 @@ +/* + * 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.r2dbc.config; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; +import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; +import org.springframework.data.auditing.config.AuditingConfiguration; +import org.springframework.data.config.ParsingUtils; +import org.springframework.data.r2dbc.mapping.event.ReactiveAuditingEntityCallback; +import org.springframework.util.Assert; + +/** + * {@link ImportBeanDefinitionRegistrar} to enable {@link EnableR2dbcAuditing} annotation. + * + * @author Mark Paluch + * @since 1.2 + */ +class R2dbcAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableR2dbcAuditing.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() + */ + @Override + protected String getAuditingHandlerBeanName() { + return "r2dbcAuditingHandler"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) + */ + @Override + protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { + + Assert.notNull(configuration, "AuditingConfiguration must not be null!"); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveIsNewAwareAuditingHandler.class); + + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(PersistentEntitiesFactoryBean.class); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + + builder.addConstructorArgValue(definition.getBeanDefinition()); + return configureDefaultAuditHandlerAttributes(configuration, builder); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListener(org.springframework.beans.factory.config.BeanDefinition, org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, + BeanDefinitionRegistry registry) { + + Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); + + builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + builder.getRawBeanDefinition().setSource(auditingHandlerDefinition.getSource()); + + registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingEntityCallback.class.getName(), + registry); + } + +} diff --git a/src/main/java/org/springframework/data/r2dbc/mapping/event/ReactiveAuditingEntityCallback.java b/src/main/java/org/springframework/data/r2dbc/mapping/event/ReactiveAuditingEntityCallback.java new file mode 100644 index 00000000..db7d3bb1 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/mapping/event/ReactiveAuditingEntityCallback.java @@ -0,0 +1,68 @@ +/* + * 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.r2dbc.mapping.event; + +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.ReactiveIsNewAwareAuditingHandler; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.util.Assert; + +/** + * Reactive {@link EntityCallback} to populate auditing related fields on an entity about to be saved. + * + * @author Mark Paluch + * @since 1.2 + */ +public class ReactiveAuditingEntityCallback implements BeforeConvertCallback, Ordered { + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link BeforeConvertCallback} 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.r2dbc.mapping.event.ReactiveBeforeConvertCallback#onBeforeConvert(java.lang.Object, SqlIdentifier) + */ + @Override + public Publisher onBeforeConvert(Object entity, SqlIdentifier table) { + return auditingHandlerFactory.getObject().markAudited(entity); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return 100; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/config/AuditingUnitTests.java b/src/test/java/org/springframework/data/r2dbc/config/AuditingUnitTests.java new file mode 100644 index 00000000..ec56a090 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/config/AuditingUnitTests.java @@ -0,0 +1,96 @@ +/* + * 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.r2dbc.config; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.domain.ReactiveAuditorAware; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; +import org.springframework.data.r2dbc.mapping.R2dbcMappingContext; +import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback; +import org.springframework.data.relational.core.sql.SqlIdentifier; + +/** + * Unit tests for {@link EnableR2dbcAuditing} + * + * @author Mark Paluch + */ +public class AuditingUnitTests { + + @EnableR2dbcAuditing(auditorAwareRef = "myAuditor") + static class AuditingConfiguration { + + @Bean + ReactiveAuditorAware myAuditor() { + return () -> Mono.just("Walter"); + } + + @Bean + R2dbcMappingContext r2dbcMappingContext() { + return new R2dbcMappingContext(); + } + } + + @Test // gh-281 + public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AuditingConfiguration.class); + + R2dbcMappingContext mappingContext = context.getBean(R2dbcMappingContext.class); + mappingContext.getPersistentEntity(Entity.class); + + ReactiveEntityCallbacks callbacks = ReactiveEntityCallbacks.create(context); + + Entity entity = new Entity(); + entity = callbacks.callback(BeforeConvertCallback.class, entity, SqlIdentifier.unquoted("table")).block(); + + assertThat(entity.created).isNotNull(); + assertThat(entity.modified).isEqualTo(entity.created); + assertThat(entity.modifiedBy).isEqualTo("Walter"); + + Thread.sleep(10); + entity.id = 1L; + + entity = callbacks.callback(BeforeConvertCallback.class, entity, SqlIdentifier.unquoted("table")).block(); + + assertThat(entity.created).isNotNull(); + assertThat(entity.modified).isNotEqualTo(entity.created); + context.close(); + } + + @Data + class Entity { + + @Id Long id; + @CreatedDate LocalDateTime created; + @LastModifiedDate LocalDateTime modified; + @LastModifiedBy String modifiedBy; + + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrarUnitTests.java b/src/test/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrarUnitTests.java new file mode 100644 index 00000000..d4596013 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/config/R2dbcAuditingRegistrarUnitTests.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.r2dbc.config; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Unit tests for {@link R2dbcAuditingRegistrar}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class R2dbcAuditingRegistrarUnitTests { + + private R2dbcAuditingRegistrar registrar = new R2dbcAuditingRegistrar(); + + @Mock AnnotationMetadata metadata; + @Mock BeanDefinitionRegistry registry; + + @Test // gh-281 + public void rejectsNullAnnotationMetadata() { + assertThatIllegalArgumentException().isThrownBy(() -> registrar.registerBeanDefinitions(null, registry)); + } + + @Test // gh-281 + public void rejectsNullBeanDefinitionRegistry() { + assertThatIllegalArgumentException().isThrownBy(() -> registrar.registerBeanDefinitions(metadata, null)); + } +}