From 86c47e80f863ec75bf2c8d3222e219e83d2b6e63 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 4 Jun 2019 11:33:34 +0200 Subject: [PATCH] DATACASS-4 - Add support for auditing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support auditing using Spring's IsNewAwareAuditingHandler. Entities that are saved are processed before persisting these through auditing EntityCallbacks. @Table class Entity { @Id Long id; @CreatedDate LocalDateTime created; @LastModifiedDate LocalDateTime modified; // … } @EnableCassandraAuditing @Configuration class MyConfiguration extends AbstractReactiveCassandraConfiguration { // … } --- ...CassandraAuditingBeanDefinitionParser.java | 112 ++ .../config/CassandraAuditingRegistrar.java | 182 ++++ .../config/CassandraNamespaceHandler.java | 1 + .../config/EnableCassandraAuditing.java | 70 ++ .../mapping/event/AuditingEntityCallback.java | 66 ++ .../event/ReactiveAuditingEntityCallback.java | 68 ++ .../main/resources/META-INF/spring.schemas | 6 +- .../cassandra/config/spring-cassandra-2.2.xsd | 975 ++++++++++++++++++ .../data/cassandra/config/spring-cql-2.2.xsd | 712 +++++++++++++ .../config/AbstractAuditingTests.java | 106 ++ .../CassandraAuditingRegistrarUnitTests.java | 48 + .../config/JavaConfigAuditingTests.java | 77 ++ .../config/NamespaceAuditingTests.java | 47 + .../AuditingEntityCallbackUnitTests.java | 138 +++ ...activeAuditingEntityCallbackUnitTests.java | 138 +++ .../config/NamespaceAuditingTests-context.xml | 26 + src/main/asciidoc/index.adoc | 2 + src/main/asciidoc/new-features.adoc | 1 + .../reference/cassandra-auditing.adoc | 33 + .../reference/cassandra-entity-callbacks.adoc | 29 + src/main/asciidoc/reference/mapping.adoc | 3 + 21 files changed, 2838 insertions(+), 2 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingBeanDefinitionParser.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/EnableCassandraAuditing.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallback.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallback.java create mode 100644 spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd create mode 100644 spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cql-2.2.xsd create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/JavaConfigAuditingTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/NamespaceAuditingTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallbackUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallbackUnitTests.java create mode 100644 spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/NamespaceAuditingTests-context.xml create mode 100644 src/main/asciidoc/reference/cassandra-auditing.adoc create mode 100644 src/main/asciidoc/reference/cassandra-entity-callbacks.adoc diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingBeanDefinitionParser.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingBeanDefinitionParser.java new file mode 100644 index 000000000..ada20fdfa --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingBeanDefinitionParser.java @@ -0,0 +1,112 @@ +/* + * Copyright 2019 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.cassandra.config; + +import static org.springframework.data.config.ParsingUtils.*; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.data.auditing.config.IsNewAwareAuditingHandlerBeanDefinitionParser; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.core.mapping.event.AuditingEntityCallback; +import org.springframework.data.cassandra.core.mapping.event.ReactiveAuditingEntityCallback; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import org.w3c.dom.Element; + +/** + * {@link BeanDefinitionParser} to register a {@link AuditingEntityCallback} to transparently set auditing information + * on an entity. Registers {@link ReactiveAuditingEntityCallback} if Project Reactor is on the class path. + * + * @author Mark Paluch + * @since 2.2 + */ +public class CassandraAuditingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono", + CassandraAuditingRegistrar.class.getClassLoader()); + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + @Override + protected Class getBeanClass(Element element) { + return AuditingEntityCallback.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#shouldGenerateId() + */ + @Override + protected boolean shouldGenerateId() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + + String mappingContextRef = element.getAttribute("mapping-context-ref"); + + if (!StringUtils.hasText(mappingContextRef)) { + + BeanDefinitionRegistry registry = parserContext.getRegistry(); + + if (!registry.containsBeanDefinition(DefaultBeanNames.CONTEXT)) { + registry.registerBeanDefinition(DefaultBeanNames.CONTEXT, + new RootBeanDefinition(CassandraMappingContext.class)); + } + + mappingContextRef = DefaultBeanNames.CONTEXT; + } + + IsNewAwareAuditingHandlerBeanDefinitionParser parser = new IsNewAwareAuditingHandlerBeanDefinitionParser( + mappingContextRef); + parser.parse(element, parserContext); + + AbstractBeanDefinition isNewAwareAuditingHandler = getObjectFactoryBeanDefinition(parser.getResolvedBeanName(), + parserContext.extractSource(element)); + builder.addConstructorArgValue(isNewAwareAuditingHandler); + + if (PROJECT_REACTOR_AVAILABLE) { + registerReactiveAuditingEntityCallback(parserContext.getRegistry(), isNewAwareAuditingHandler, + parserContext.extractSource(element)); + } + } + + private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, + AbstractBeanDefinition isNewAwareAuditingHandler, @Nullable Object source) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); + + builder.addConstructorArgValue(isNewAwareAuditingHandler); + builder.getRawBeanDefinition().setSource(source); + + registry.registerBeanDefinition(ReactiveAuditingEntityCallback.class.getName(), builder.getBeanDefinition()); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java new file mode 100644 index 000000000..406eebcdc --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java @@ -0,0 +1,182 @@ +/* + * Copyright 2019 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.cassandra.config; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.FactoryBean; +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.core.type.AnnotationMetadata; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; +import org.springframework.data.auditing.config.AuditingConfiguration; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; +import org.springframework.data.cassandra.core.mapping.event.AuditingEntityCallback; +import org.springframework.data.cassandra.core.mapping.event.ReactiveAuditingEntityCallback; +import org.springframework.data.config.ParsingUtils; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link ImportBeanDefinitionRegistrar} to enable {@link EnableCassandraAuditing} annotation. + * + * @author Mark Paluch + * @since 2.2 + */ +class CassandraAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono", + CassandraAuditingRegistrar.class.getClassLoader()); + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableCassandraAuditing.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() + */ + @Override + protected String getAuditingHandlerBeanName() { + return "cassandraAuditingHandler"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { + + Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + super.registerBeanDefinitions(annotationMetadata, registry); + } + + /* + * (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(IsNewAwareAuditingHandler.class); + + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(CassandraMappingContextLookup.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 listenerBeanDefinitionBuilder = BeanDefinitionBuilder + .rootBeanDefinition(AuditingEntityCallback.class); + listenerBeanDefinitionBuilder + .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + + registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), + AuditingEntityCallback.class.getName(), registry); + + if (PROJECT_REACTOR_AVAILABLE) { + registerReactiveAuditingEntityCallback(registry, auditingHandlerDefinition.getSource()); + } + } + + private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, Object source) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); + + builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + builder.getRawBeanDefinition().setSource(source); + + registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingEntityCallback.class.getName(), + registry); + } + + /** + * Simple helper to be able to wire the {@link MappingContext} from a + * {@link org.springframework.data.cassandra.core.convert.MappingCassandraConverter} bean available in the application + * context. + */ + static class CassandraMappingContextLookup + implements FactoryBean, CassandraPersistentProperty>> { + + private final MappingCassandraConverter converter; + + /** + * Creates a new {@link CassandraMappingContextLookup} for the given {@link MappingCassandraConverter}. + * + * @param converter must not be {@literal null}. + */ + public CassandraMappingContextLookup(MappingCassandraConverter converter) { + this.converter = converter; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + @Override + public MappingContext, CassandraPersistentProperty> getObject() + throws Exception { + return converter.getMappingContext(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + @Override + public Class getObjectType() { + return MappingContext.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#isSingleton() + */ + @Override + public boolean isSingleton() { + return true; + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraNamespaceHandler.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraNamespaceHandler.java index 380cb275a..133ec5695 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraNamespaceHandler.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraNamespaceHandler.java @@ -37,6 +37,7 @@ public class CassandraNamespaceHandler extends NamespaceHandlerSupport { registerBeanDefinitionParser("cluster", new CassandraClusterParser()); registerBeanDefinitionParser("session", new CassandraSessionParser()); registerBeanDefinitionParser("template", new CassandraTemplateParser()); + registerBeanDefinitionParser("auditing", new CassandraAuditingBeanDefinitionParser()); registerBeanDefinitionParser("converter", new CassandraMappingConverterParser()); registerBeanDefinitionParser("mapping", new CassandraMappingContextParser()); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/EnableCassandraAuditing.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/EnableCassandraAuditing.java new file mode 100644 index 000000000..29220b2f9 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/EnableCassandraAuditing.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 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.cassandra.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.AuditorAware; + +/** + * Annotation to enable auditing in Cassandra via annotation configuration. + * + * @author Mark Paluch + * @since 2.2 + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(CassandraAuditingRegistrar.class) +public @interface EnableCassandraAuditing { + + /** + * Configures the {@link AuditorAware} bean to be used to lookup the current principal. + * + * @return + */ + String auditorAwareRef() default ""; + + /** + * Configures whether the creation and modification dates are set. Defaults to {@literal true}. + * + * @return + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. + * + * @return + */ + boolean modifyOnCreate() default true; + + /** + * Configures a {@link DateTimeProvider} bean name that allows customizing the + * {@link java.time.temporal.TemporalAccessor} to be used for setting creation and modification dates. + * + * @return + */ + String dateTimeProviderRef() default ""; +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallback.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallback.java new file mode 100644 index 000000000..6cf96b362 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallback.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 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.cassandra.core.mapping.event; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; + +/** + * {@link EntityCallback} to populate auditing related fields on an entity about to be saved. + * + * @author Mark Paluch + * @since 2.2 + */ +public class AuditingEntityCallback implements BeforeConvertCallback, Ordered { + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link AuditingEntityCallback} using the given {@link MappingContext} and {@link AuditingHandler} + * provided by the given {@link ObjectFactory}. + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public AuditingEntityCallback(ObjectFactory auditingHandlerFactory) { + + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + this.auditingHandlerFactory = auditingHandlerFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.mapping.event.BeforeConvertCallback#onBeforeConvert(java.lang.Object, org.springframework.data.cassandra.core.cql.CqlIdentifier) + */ + @Override + public Object onBeforeConvert(Object entity, CqlIdentifier tableName) { + return auditingHandlerFactory.getObject().markAudited(entity); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return 100; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallback.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallback.java new file mode 100644 index 000000000..2958d23a6 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallback.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 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.cassandra.core.mapping.event; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; + +/** + * Reactive {@link EntityCallback} to populate auditing related fields on an entity about to be saved. + * + * @author Mark Paluch + * @since 2.2 + */ +public class ReactiveAuditingEntityCallback implements ReactiveBeforeConvertCallback, Ordered { + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link ReactiveAuditingEntityCallback} 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.cassandra.core.mapping.event.ReactiveBeforeConvertCallback#onBeforeConvert(java.lang.Object, org.springframework.data.cassandra.core.cql.CqlIdentifier) + */ + @Override + public Mono onBeforeConvert(Object entity, CqlIdentifier tableName) { + return Mono.just(auditingHandlerFactory.getObject().markAudited(entity)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return 100; + } +} diff --git a/spring-data-cassandra/src/main/resources/META-INF/spring.schemas b/spring-data-cassandra/src/main/resources/META-INF/spring.schemas index 897f535e2..8a0b5aaac 100644 --- a/spring-data-cassandra/src/main/resources/META-INF/spring.schemas +++ b/spring-data-cassandra/src/main/resources/META-INF/spring.schemas @@ -1,9 +1,11 @@ http\://www.springframework.org/schema/cql/spring-cql-1.0.xsd=org/springframework/data/cassandra/config/spring-cql-1.0.xsd http\://www.springframework.org/schema/cql/spring-cql-1.5.xsd=org/springframework/data/cassandra/config/spring-cql-1.5.xsd http\://www.springframework.org/schema/cql/spring-cql-2.0.xsd=org/springframework/data/cassandra/config/spring-cql-2.0.xsd -http\://www.springframework.org/schema/cql/spring-cql.xsd=org/springframework/data/cassandra/config/spring-cql-2.0.xsd +http\://www.springframework.org/schema/cql/spring-cql-2.2.xsd=org/springframework/data/cassandra/config/spring-cql-2.2.xsd +http\://www.springframework.org/schema/cql/spring-cql.xsd=org/springframework/data/cassandra/config/spring-cql-2.2.xsd http\://www.springframework.org/schema/data/cassandra/spring-cassandra-1.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-1.0.xsd http\://www.springframework.org/schema/data/cassandra/spring-cassandra-1.5.xsd=org/springframework/data/cassandra/config/spring-cassandra-1.5.xsd http\://www.springframework.org/schema/data/cassandra/spring-cassandra-2.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.0.xsd -http\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.0.xsd +http\://www.springframework.org/schema/data/cassandra/spring-cassandra-2.2.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd +http\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd diff --git a/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd new file mode 100644 index 000000000..e037806ac --- /dev/null +++ b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd @@ -0,0 +1,975 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cql-2.2.xsd b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cql-2.2.xsd new file mode 100644 index 000000000..7e73e8d1c --- /dev/null +++ b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cql-2.2.xsd @@ -0,0 +1,712 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/AbstractAuditingTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/AbstractAuditingTests.java index e69de29bb..34aa8d02e 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/AbstractAuditingTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/AbstractAuditingTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 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.cassandra.config; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; + +/** + * Abstract integration tests for the auditing support. + * + * @author Mark Paluch + */ +public abstract class AbstractAuditingTests { + + @Test // DATACASS-4 + public void enablesAuditingAndSetsPropertiesAccordingly() throws Exception { + + ApplicationContext context = getApplicationContext(); + + CassandraMappingContext mappingContext = context.getBean(CassandraMappingContext.class); + mappingContext.getPersistentEntity(Entity.class); + + EntityCallbacks callbacks = EntityCallbacks.create(context); + + Entity entity = new Entity(); + entity = callbacks.callback(BeforeConvertCallback.class, entity, CqlIdentifier.of("entity")); + + assertThat(entity.created).isNotNull(); + assertThat(entity.modified).isEqualTo(entity.created); + + Thread.sleep(10); + entity.id = 1L; + + entity = callbacks.callback(BeforeConvertCallback.class, entity, CqlIdentifier.of("entity")); + + assertThat(entity.created).isNotNull(); + assertThat(entity.modified).isAfter(entity.created); + } + + @Test // DATACASS-4 + public void enablesReactiveAuditingAndSetsPropertiesAccordingly() throws Exception { + + ApplicationContext context = getApplicationContext(); + + CassandraMappingContext mappingContext = context.getBean(CassandraMappingContext.class); + mappingContext.getPersistentEntity(Entity.class); + + ReactiveEntityCallbacks callbacks = ReactiveEntityCallbacks.create(context); + + Entity entity = new Entity(); + entity = callbacks.callback(BeforeConvertCallback.class, entity, CqlIdentifier.of("entity")).block(); + + assertThat(entity.created).isNotNull(); + assertThat(entity.modified).isEqualTo(entity.created); + + Thread.sleep(10); + entity.id = 1L; + + entity = callbacks.callback(BeforeConvertCallback.class, entity, CqlIdentifier.of("entity")).block(); + + assertThat(entity.created).isNotNull(); + assertThat(entity.modified).isAfter(entity.created); + } + + protected abstract ApplicationContext getApplicationContext(); + + @Table + class Entity { + + @Id Long id; + @CreatedDate LocalDateTime created; + LocalDateTime modified; + + @LastModifiedDate + public LocalDateTime getModified() { + return modified; + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarUnitTests.java new file mode 100644 index 000000000..7c205b959 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarUnitTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 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.cassandra.config; + +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 CassandraAuditingRegistrar}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class CassandraAuditingRegistrarUnitTests { + + CassandraAuditingRegistrar registrar = new CassandraAuditingRegistrar(); + + @Mock AnnotationMetadata metadata; + @Mock BeanDefinitionRegistry registry; + + @Test(expected = IllegalArgumentException.class) // DATACASS-4 + public void rejectsNullAnnotationMetadata() { + registrar.registerBeanDefinitions(null, registry); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-4 + public void rejectsNullBeanDefinitionRegistry() { + registrar.registerBeanDefinitions(metadata, null); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/JavaConfigAuditingTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/JavaConfigAuditingTests.java new file mode 100644 index 000000000..dea0ac5b1 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/JavaConfigAuditingTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 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.cassandra.config; + +import static org.mockito.Mockito.*; + +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; + +/** + * Unit tests for auditing enabled using Java config. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class JavaConfigAuditingTests extends AbstractAuditingTests { + + @Autowired ApplicationContext context; + + @Override + protected ApplicationContext getApplicationContext() { + return context; + } + + @EnableCassandraAuditing + @Configuration + static class TestConfig extends AbstractReactiveCassandraConfiguration { + + @Override + protected String getKeyspaceName() { + return "foo"; + } + + @Bean + public CassandraSessionFactoryBean session() { + + CassandraSessionFactoryBean sessionFactoryBean = mock(CassandraSessionFactoryBean.class); + Session session = mock(Session.class); + when(sessionFactoryBean.getObject()).thenReturn(session); + + return sessionFactoryBean; + } + + @Bean + public CassandraClusterFactoryBean cluster() { + + CassandraClusterFactoryBean cassandraClusterFactoryBean = mock(CassandraClusterFactoryBean.class); + Cluster cluster = mock(Cluster.class); + when(cassandraClusterFactoryBean.getObject()).thenReturn(cluster); + + return cassandraClusterFactoryBean; + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/NamespaceAuditingTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/NamespaceAuditingTests.java new file mode 100644 index 000000000..d4eff7ce3 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/NamespaceAuditingTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 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.cassandra.config; + +import org.junit.ClassRule; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.cassandra.test.util.CassandraRule; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Unit tests for auditing enabled using XML config. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class NamespaceAuditingTests extends AbstractAuditingTests { + + /** + * Initiate a Cassandra environment in this test scope. + */ + @ClassRule public static final CassandraRule cassandraEnvironment = new CassandraRule("embedded-cassandra.yaml"); + + @Autowired ApplicationContext context; + + @Override + protected ApplicationContext getApplicationContext() { + return context; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallbackUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallbackUnitTests.java new file mode 100644 index 000000000..f9deb1961 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/AuditingEntityCallbackUnitTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2019 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.cassandra.core.mapping.event; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Wither; + +import java.util.Collections; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.core.Ordered; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.mapping.context.PersistentEntities; + +/** + * Unit tests for {@link AuditingEntityCallback}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class AuditingEntityCallbackUnitTests { + + IsNewAwareAuditingHandler handler; + AuditingEntityCallback callback; + + @Before + public void setUp() { + + CassandraMappingContext mappingContext = new CassandraMappingContext(); + mappingContext.getPersistentEntity(Sample.class); + + handler = spy(new IsNewAwareAuditingHandler(new PersistentEntities(Collections.singletonList(mappingContext)))); + + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markCreated(any()); + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markModified(any()); + + callback = new AuditingEntityCallback(() -> handler); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-4 + public void rejectsNullAuditingHandler() { + new AuditingEntityCallback(null); + } + + @Test // DATACASS-4 + public void triggersCreationMarkForObjectWithEmptyId() { + + Sample sample = new Sample(); + callback.onBeforeConvert(sample, CqlIdentifier.of("foo")); + + verify(handler, times(1)).markCreated(sample); + verify(handler, times(0)).markModified(any()); + } + + @Test // DATACASS-4 + public void triggersModificationMarkForObjectWithSetId() { + + Sample sample = new Sample(); + sample.id = "id"; + callback.onBeforeConvert(sample, CqlIdentifier.of("foo")); + + verify(handler, times(0)).markCreated(any()); + verify(handler, times(1)).markModified(sample); + } + + @Test // DATACASS-4 + public void hasExplicitOrder() { + + assertThat(callback, is(instanceOf(Ordered.class))); + assertThat(callback.getOrder(), is(100)); + } + + @Test // DATACASS-4 + public void propagatesChangedInstanceToEvent() { + + ImmutableSample sample = new ImmutableSample(); + + ImmutableSample newSample = new ImmutableSample(); + IsNewAwareAuditingHandler handler = mock(IsNewAwareAuditingHandler.class); + doReturn(newSample).when(handler).markAudited(eq(sample)); + + AuditingEntityCallback listener = new AuditingEntityCallback(() -> handler); + Object result = listener.onBeforeConvert(sample, CqlIdentifier.of("foo")); + + assertThat(result).isSameAs(newSample); + } + + static class Sample { + + @Id String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; + } + + @Value + @Wither + @AllArgsConstructor + @NoArgsConstructor(force = true) + static class ImmutableSample { + + @Id String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallbackUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallbackUnitTests.java new file mode 100644 index 000000000..b64ae5d0b --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/event/ReactiveAuditingEntityCallbackUnitTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2019 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.cassandra.core.mapping.event; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Wither; + +import java.util.Collections; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.core.Ordered; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.mapping.context.PersistentEntities; + +/** + * Unit tests for {@link ReactiveAuditingEntityCallback}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class ReactiveAuditingEntityCallbackUnitTests { + + IsNewAwareAuditingHandler handler; + ReactiveAuditingEntityCallback callback; + + @Before + public void setUp() { + + CassandraMappingContext mappingContext = new CassandraMappingContext(); + mappingContext.getPersistentEntity(Sample.class); + + handler = spy(new IsNewAwareAuditingHandler(new PersistentEntities(Collections.singletonList(mappingContext)))); + + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markCreated(any()); + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markModified(any()); + + callback = new ReactiveAuditingEntityCallback(() -> handler); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-4 + public void rejectsNullAuditingHandler() { + new AuditingEntityCallback(null); + } + + @Test // DATACASS-4 + public void triggersCreationMarkForObjectWithEmptyId() { + + Sample sample = new Sample(); + callback.onBeforeConvert(sample, CqlIdentifier.of("foo")); + + verify(handler, times(1)).markCreated(sample); + verify(handler, times(0)).markModified(any()); + } + + @Test // DATACASS-4 + public void triggersModificationMarkForObjectWithSetId() { + + Sample sample = new Sample(); + sample.id = "id"; + callback.onBeforeConvert(sample, CqlIdentifier.of("foo")); + + verify(handler, times(0)).markCreated(any()); + verify(handler, times(1)).markModified(sample); + } + + @Test // DATACASS-4 + public void hasExplicitOrder() { + + assertThat(callback, is(instanceOf(Ordered.class))); + assertThat(callback.getOrder(), is(100)); + } + + @Test // DATACASS-4 + public void propagatesChangedInstanceToEvent() { + + ImmutableSample sample = new ImmutableSample(); + + ImmutableSample newSample = new ImmutableSample(); + IsNewAwareAuditingHandler handler = mock(IsNewAwareAuditingHandler.class); + doReturn(newSample).when(handler).markAudited(eq(sample)); + + ReactiveAuditingEntityCallback listener = new ReactiveAuditingEntityCallback(() -> handler); + Object result = listener.onBeforeConvert(sample, CqlIdentifier.of("foo")).block(); + + assertThat(result).isSameAs(newSample); + } + + static class Sample { + + @Id String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; + } + + @Value + @Wither + @AllArgsConstructor + @NoArgsConstructor(force = true) + static class ImmutableSample { + + @Id String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; + } +} diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/NamespaceAuditingTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/NamespaceAuditingTests-context.xml new file mode 100644 index 000000000..b332bbe3d --- /dev/null +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/NamespaceAuditingTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index fd38b96be..47d106ae6 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -27,6 +27,8 @@ include::reference/converters.adoc[leveloffset=+1] include::reference/reactive-cassandra.adoc[leveloffset=+1] include::reference/cassandra-repositories.adoc[leveloffset=+1] include::reference/reactive-cassandra-repositories.adoc[leveloffset=+1] +include::{spring-data-commons-docs}/auditing.adoc[leveloffset=+1] +include::reference/cassandra-auditing.adoc[leveloffset=+1] include::reference/mapping.adoc[leveloffset=+1] [[appendix]] diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index ec84e252e..97cb3e639 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -11,6 +11,7 @@ This chapter summarizes changes and new features for each release. * Lightweight transaction support via `DeleteOptions` using the Template API. * Filter conditions for lightweight transaction update and delete (`UPDATE … IF `, `DELETE … IF `). * Optimistic Locking support. +* Auditing via `@EnableCassandraAuditing`. [[new-features.2-1-0]] == What's new in Spring Data for Apache Cassandra 2.1 diff --git a/src/main/asciidoc/reference/cassandra-auditing.adoc b/src/main/asciidoc/reference/cassandra-auditing.adoc new file mode 100644 index 000000000..44e7f57d7 --- /dev/null +++ b/src/main/asciidoc/reference/cassandra-auditing.adoc @@ -0,0 +1,33 @@ +[[cassandra.auditing]] +== General Auditing Configuration for Cassandra + +To activate auditing functionality, add the Spring Data for Apache Cassandra `auditing` namespace element to your configuration, as the following example shows: + +.Activating auditing by using XML configuration +==== +[source,xml] +---- + +---- +==== + +Alternatively, auditing can be enabled by annotating a configuration class with the `@EnableCassandraAuditing` annotation, as the following example shows: + +.Activating auditing using JavaConfig +==== +[source,java] +---- +@Configuration +@EnableCassandraAuditing +class Config { + + @Bean + public AuditorAware myAuditorProvider() { + return new AuditorAwareImpl(); + } +} +---- +==== + +If you expose a bean of type `AuditorAware` 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 `@EnableCassandraAuditing`. diff --git a/src/main/asciidoc/reference/cassandra-entity-callbacks.adoc b/src/main/asciidoc/reference/cassandra-entity-callbacks.adoc new file mode 100644 index 000000000..7b5c6e993 --- /dev/null +++ b/src/main/asciidoc/reference/cassandra-entity-callbacks.adoc @@ -0,0 +1,29 @@ += Store specific EntityCallbacks + +Spring Data for Apache Cassandra uses the `EntityCallback` API for its auditing support and reacts on the following callbacks. + +.Supported Entity Callbacks +[%header,cols="4"] +|=== +| Callback +| Method +| Description +| Order + +| Reactive/BeforeConvertCallback +| onBeforeConvert(T entity, String collection) +| Invoked before a domain object is converted to `org.bson.Document`. +| `Ordered.LOWEST_PRECEDENCE` + +| Reactive/AuditingEntityCallback +| onBeforeConvert(Object entity, String collection) +| Marks an auditable entity _created_ or _modified_ +| 100 + +| Reactive/BeforeSaveCallback +| onBeforeSave(T entity, org.bson.Document target, String collection) +| Invoked before a domain object is saved. + + Can modify the target, to be persisted, `Document` containing all mapped entity information. +| `Ordered.LOWEST_PRECEDENCE` + +|=== diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index d02ff4ea7..048b0d687 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -655,3 +655,6 @@ The `AbstractCassandraEventListener` has the following callback methods: * `onAfterConvert`: Called in the `CassandraTemplate.select(…)`, `.slice(…)`, and `.stream(…)` methods after converting a row retrieved from the database to a POJO. NOTE: Lifecycle events are emitted only for root-level types. Complex types used as properties within an aggregate root are not subject to event publication. + +include::../{spring-data-commons-docs}/entity-callbacks.adoc[leveloffset=+1] +include::./cassandra-entity-callbacks.adoc[leveloffset=+2]