diff --git a/src/main/java/org/springframework/data/jdbc/domain/support/JdbcAuditingEventListener.java b/src/main/java/org/springframework/data/jdbc/domain/support/JdbcAuditingEventListener.java new file mode 100644 index 0000000000..f500e9b626 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/domain/support/JdbcAuditingEventListener.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018 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 + * + * http://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.jdbc.domain.support; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.jdbc.mapping.event.BeforeSaveEvent; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Spring JDBC event listener to capture auditing information on persisting and updating entities. + *
+ * You can enable this class just a matter of activating auditing using {@link org.springframework.data.jdbc.repository.config.EnableJdbcAuditing} in your Spring config: + * + *
+ * @Configuration + * @EnableJdbcRepositories + * @EnableJdbcAuditing + * class JdbcRepositoryConfig { + * } + *+ * + * @author Kazuki Shimizu + * @see org.springframework.data.jdbc.repository.config.EnableJdbcAuditing + * @since 1.0 + */ +public class JdbcAuditingEventListener implements ApplicationListener
+ * @Configuration + * @EnableJdbcRepositories + * @EnableJdbcAuditing + * class JdbcRepositoryConfig { + * } + *+ * + *
+ * Note: This feature cannot use to a entity that implements {@link org.springframework.data.domain.Auditable} + * because the Spring Data JDBC does not support an {@link java.util.Optional} property yet. + *
+ * + * @see EnableJdbcRepositories + * @author Kazuki Shimizu + * @since 1.0 + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(JdbcAuditingRegistrar.class) +public @interface EnableJdbcAuditing { + + /** + * Configures the {@link AuditorAware} bean to be used to lookup the current principal. + * + * @return + * @see AuditorAware + */ + String auditorAwareRef() default ""; + + /** + * Configures whether the creation and modification dates are set. + * + * @return + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. + * + * @return + */ + boolean modifyOnCreate() default true; + + /** + * Configures a {@link DateTimeProvider} bean name that allows customizing the {@link java.time.LocalDateTime} to be + * used for setting creation and modification dates. + * + * @return + * @see DateTimeProvider + */ + String dateTimeProviderRef() default ""; + +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java new file mode 100644 index 0000000000..a734592ff5 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018 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 + * + * http://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.jdbc.repository.config; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +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.config.AuditingBeanDefinitionRegistrarSupport; +import org.springframework.data.auditing.config.AuditingConfiguration; +import org.springframework.data.config.ParsingUtils; +import org.springframework.data.jdbc.domain.support.JdbcAuditingEventListener; + +/** + * {@link ImportBeanDefinitionRegistrar} to enable {@link EnableJdbcAuditing} annotation. + * + * @see EnableJdbcAuditing + * @author Kazuki Shimizu + * @since 1.0 + */ +class JdbcAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + /** + * {@inheritDoc} + * @return return the {@link EnableJdbcAuditing} + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class extends Annotation> getAnnotation() { + return EnableJdbcAuditing.class; + } + + /** + * {@inheritDoc} + * @return return "{@literal jdbcAuditingHandler}" + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() + */ + @Override + protected String getAuditingHandlerBeanName() { + return "jdbcAuditingHandler"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) + */ + @Override + protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { + BeanDefinitionBuilder builder = super.getAuditHandlerBeanDefinitionBuilder(configuration); + return builder.addConstructorArgReference("jdbcMappingContext"); + } + + /** + * Register the bean definition of {@link JdbcAuditingEventListener}. + * {@inheritDoc} + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListenerBeanDefinition(BeanDefinition, BeanDefinitionRegistry) + */ + @Override + protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, + BeanDefinitionRegistry registry) { + Class> listenerClass = JdbcAuditingEventListener.class; + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(listenerClass); + builder.addPropertyValue("auditingHandler", + ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), null)); + registerInfrastructureBeanWithId(builder.getRawBeanDefinition(), listenerClass.getName(), registry); + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java new file mode 100644 index 0000000000..8565ddaefe --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java @@ -0,0 +1,363 @@ +/* + * Copyright 2018 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 + * + * http://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.jdbc.repository.config; + +import lombok.Data; +import org.junit.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Primary; +import org.springframework.data.annotation.CreatedBy; +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.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jdbc.mapping.model.NamingStrategy; +import org.springframework.data.repository.CrudRepository; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the {@link EnableJdbcAuditing} annotation. + * + * @author Kazuki Shimizu + */ +public class EnableJdbcAuditingHsqlIntegrationTests { + + @Test + public void auditForAnnotatedEntity() throws InterruptedException { + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, AuditingConfiguration.class)) { + + AuditingAnnotatedDummyEntityRepository repository = context.getBean(AuditingAnnotatedDummyEntityRepository.class); + + AuditingConfiguration.currentAuditor = "user01"; + LocalDateTime now = LocalDateTime.now(); + + AuditingAnnotatedDummyEntity entity = new AuditingAnnotatedDummyEntity(); + entity.setDateOfBirth(LocalDate.of(2000, 12, 4)); + AuditingName name = new AuditingName(); + name.setFirst("Spring"); + name.setLast("Data"); + entity.setName(name); +// { +// AuditingEmail email = new AuditingEmail(); +// email.setType("mobile"); +// email.setAddress("test@spring.mobile"); +// entity.getEmails().add(email); +// } +// { +// AuditingEmail email = new AuditingEmail(); +// email.setType("pc"); +// email.setAddress("test@spring.pc"); +// entity.getEmails().add(email); +// } + + repository.save(entity); + + assertThat(entity.getId()).isNotNull(); + assertThat(entity.getCreatedBy()).isEqualTo("user01"); + assertThat(entity.getCreatedDate()).isAfter(now); + assertThat(entity.getLastModifiedBy()).isEqualTo("user01"); + assertThat(entity.getLastModifiedDate()).isAfterOrEqualTo(entity.getCreatedDate()); + assertThat(entity.getLastModifiedDate()).isAfter(now); + assertThat(entity.getName().getId()).isNotNull(); + assertThat(entity.getName().getCreatedBy()).isEqualTo("user01"); + assertThat(entity.getName().getCreatedDate()).isAfter(now); + assertThat(entity.getName().getLastModifiedBy()).isEqualTo("user01"); + assertThat(entity.getName().getLastModifiedDate()).isAfterOrEqualTo(entity.getName().getCreatedDate()); + assertThat(entity.getName().getLastModifiedDate()).isAfter(now); +// assertThat(entity.getEmails().get(0).getId()).isNotNull(); +// assertThat(entity.getEmails().get(0).getCreatedBy()).isEqualTo("user01"); +// assertThat(entity.getEmails().get(0).getCreatedDate()).isAfter(now); +// assertThat(entity.getEmails().get(0).getLastModifiedBy()).isEqualTo("user01"); +// assertThat(entity.getEmails().get(0).getLastModifiedDate()).isAfterOrEqualTo(entity.getEmails().get(0).getCreatedDate()); +// assertThat(entity.getEmails().get(0).getLastModifiedDate()).isAfter(now); +// assertThat(entity.getEmails().get(1).getId()).isNotNull(); +// assertThat(entity.getEmails().get(1).getCreatedBy()).isEqualTo("user01"); +// assertThat(entity.getEmails().get(1).getCreatedDate()).isAfter(now); +// assertThat(entity.getEmails().get(1).getLastModifiedBy()).isEqualTo("user01"); +// assertThat(entity.getEmails().get(1).getLastModifiedDate()).isAfterOrEqualTo(entity.getEmails().get(0).getCreatedDate()); +// assertThat(entity.getEmails().get(1).getLastModifiedDate()).isAfter(now); + assertThat(repository.findById(entity.getId()).get()).isEqualTo(entity); + + LocalDateTime beforeCreatedDate = entity.getCreatedDate(); + LocalDateTime beforeLastModifiedDate = entity.getLastModifiedDate(); + + TimeUnit.MILLISECONDS.sleep(100); + AuditingConfiguration.currentAuditor = "user02"; + + name.setFirst("Spring"); + name.setLast("Data JDBC"); + repository.save(entity); + + assertThat(entity.getCreatedBy()).isEqualTo("user01"); + assertThat(entity.getCreatedDate()).isEqualTo(beforeCreatedDate); + assertThat(entity.getLastModifiedBy()).isEqualTo("user02"); + assertThat(entity.getLastModifiedDate()).isAfter(beforeLastModifiedDate); + assertThat(entity.getName().getCreatedBy()).isEqualTo("user01"); + assertThat(entity.getName().getCreatedDate()).isEqualTo(beforeCreatedDate); + assertThat(entity.getName().getLastModifiedBy()).isEqualTo("user02"); + assertThat(entity.getName().getLastModifiedDate()).isAfter(beforeLastModifiedDate); + assertThat(repository.findById(entity.getId()).get()).isEqualTo(entity); + } + } + + @Test + public void noAnnotatedEntity() { + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, AuditingConfiguration.class)) { + + DummyEntityRepository repository = context.getBean(DummyEntityRepository.class); + + DummyEntity entity = new DummyEntity(); + entity.setDateOfBirth(LocalDate.of(2000, 12, 4)); + Name name = new Name(); + name.setFirst("Spring"); + name.setLast("Data"); + entity.setName(name); + { + Email email = new Email(); + email.setType("mobile"); + email.setAddress("test@spring.mobile"); + entity.getEmails().add(email); + } + { + Email email = new Email(); + email.setType("pc"); + email.setAddress("test@spring.pc"); + entity.getEmails().add(email); + } + + repository.save(entity); + + assertThat(entity.getId()).isNotNull(); + assertThat(entity.getName().getId()).isNotNull(); + assertThat(entity.getEmails().get(0).getId()).isNotNull(); + assertThat(entity.getEmails().get(1).getId()).isNotNull(); + assertThat(repository.findById(entity.getId()).get()).isEqualTo(entity); + + name.setFirst("Spring"); + name.setLast("Data JDBC"); + + repository.save(entity); + + assertThat(repository.findById(entity.id).get()).isEqualTo(entity); + } + } + + @Test + public void customizeEnableJdbcAuditingAttributes() { + // Test for 'auditorAwareRef', 'dateTimeProviderRef' and 'modifyOnCreate' + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, CustomizeAuditingConfiguration1.class)) { + AuditingAnnotatedDummyEntityRepository repository = context.getBean(AuditingAnnotatedDummyEntityRepository.class); + + LocalDateTime currentDateTime = LocalDate.of(2018, 4, 14).atStartOfDay(); + CustomizeAuditingConfiguration1.currentDateTime = currentDateTime; + + AuditingAnnotatedDummyEntity entity = new AuditingAnnotatedDummyEntity(); + AuditingName name = new AuditingName(); + name.setFirst("Spring"); + name.setLast("Data JDBC"); + entity.setName(name); + + repository.save(entity); + + assertThat(entity.getId()).isNotNull(); + assertThat(entity.getCreatedBy()).isEqualTo("custom user"); + assertThat(entity.getCreatedDate()).isEqualTo(currentDateTime); + assertThat(entity.getLastModifiedBy()).isNull(); + assertThat(entity.getLastModifiedDate()).isNull(); + } + // Test for 'setDates' + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, CustomizeAuditingConfiguration2.class)) { + AuditingAnnotatedDummyEntityRepository repository = context.getBean(AuditingAnnotatedDummyEntityRepository.class); + + AuditingAnnotatedDummyEntity entity = new AuditingAnnotatedDummyEntity(); + AuditingName name = new AuditingName(); + name.setFirst("Spring"); + name.setLast("Data JDBC"); + entity.setName(name); + + repository.save(entity); + + assertThat(entity.getId()).isNotNull(); + assertThat(entity.getCreatedBy()).isEqualTo("user"); + assertThat(entity.getCreatedDate()).isNull(); + assertThat(entity.getLastModifiedBy()).isEqualTo("user"); + assertThat(entity.getLastModifiedDate()).isNull(); + } + } + + + interface AuditingAnnotatedDummyEntityRepository extends CrudRepository