DATACOUCH-91 - Support auditing (via java config)

This commit adds support for auditing via the use of the four annotations (CreatedBy, LastModifiedBy, CreationDate, LastModificationDate), enabled via the EnableCouchbaseAuditing annotation.

As couchbase entities MUST have an id, the only case where the creation event can be detected is when the entity has a Version annotated field.
This commit is contained in:
Simon Baslé
2016-02-23 18:58:29 +01:00
parent dea461458b
commit 39fb52d2a6
10 changed files with 569 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
package org.springframework.data.couchbase.repository.auditing;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.IntegrationTestApplicationConfig;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
@Configuration
@EnableCouchbaseRepositories
@EnableCouchbaseAuditing(modifyOnCreate = false)
public class AuditedApplicationConfig extends IntegrationTestApplicationConfig {
@Bean
public AuditedAuditorAware couchbaseAuditorAware() {
return new AuditedAuditorAware();
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.data.couchbase.repository.auditing;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
public class AuditedAuditorAware implements AuditorAware<String> {
private String auditor = "auditor";
@Override
public String getCurrentAuditor() {
return auditor;
}
public void setAuditor(String auditor) {
this.auditor = auditor;
}
}

View File

@@ -0,0 +1,129 @@
package org.springframework.data.couchbase.repository.auditing;
import java.util.Date;
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.annotation.Version;
import org.springframework.data.couchbase.core.mapping.Document;
@Document
public class AuditedItem {
@Id
private final String id;
private String value;
@CreatedBy
private String creator;
@LastModifiedBy
private String lastModifiedBy;
@LastModifiedDate
private Date lastModification;
@CreatedDate
private Date creationDate;
@Version
private long version;
public AuditedItem(String id, String value) {
this.id = id;
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Date getLastModification() {
return lastModification;
}
public void setLastModification(Date lastModification) {
this.lastModification = lastModification;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
@Override
public String toString() {
return "AuditedItem{" +
"id='" + id + '\'' +
", value='" + value + '\'' +
", creator='" + creator + '\'' +
", lastModifiedBy='" + lastModifiedBy + '\'' +
", lastModification=" + lastModification +
", creationDate=" + creationDate +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AuditedItem that = (AuditedItem) o;
if (!id.equals(that.id)) return false;
if (!value.equals(that.value)) return false;
if (creator != null ? !creator.equals(that.creator) : that.creator != null) return false;
if (lastModifiedBy != null ? !lastModifiedBy.equals(that.lastModifiedBy) : that.lastModifiedBy != null)
return false;
if (lastModification != null ? !lastModification.equals(that.lastModification) : that.lastModification != null)
return false;
return creationDate != null ? creationDate.equals(that.creationDate) : that.creationDate == null;
}
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + value.hashCode();
result = 31 * result + (creator != null ? creator.hashCode() : 0);
result = 31 * result + (lastModifiedBy != null ? lastModifiedBy.hashCode() : 0);
result = 31 * result + (lastModification != null ? lastModification.hashCode() : 0);
result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0);
return result;
}
}

View File

@@ -0,0 +1,7 @@
package org.springframework.data.couchbase.repository.auditing;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
public interface AuditedRepository extends CouchbaseRepository<AuditedItem, String> {
}

View File

@@ -0,0 +1,89 @@
package org.springframework.data.couchbase.repository.auditing;
import static org.junit.Assert.*;
import java.util.Date;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Simon Baslé
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AuditedApplicationConfig.class)
public class AuditingTests {
@Autowired
private AuditedRepository repository;
@Autowired
private AuditedAuditorAware auditorAware;
private static final String KEY = "auditedTest";
@After
public void cleanupAuditedEntity() {
repository.getCouchbaseOperations().getCouchbaseBucket().remove(KEY);
}
@Test
public void testCreationEventIsRegistered() {
assertFalse(repository.exists(KEY));
Date start = new Date();
AuditedItem item = new AuditedItem(KEY, "creation");
auditorAware.setAuditor("auditor");
repository.save(item);
AuditedItem persisted = repository.findOne(KEY);
assertNotNull("expected entity to be persisted", persisted);
assertNotNull("expected creation date audit trail", persisted.getCreationDate());
assertEquals("expected creation user audit trail", "auditor", persisted.getCreator());
assertTrue("creation date is too early", persisted.getCreationDate().after(start));
assertTrue("creation date is too late", persisted.getCreationDate().before(new Date()));
assertNull("expected modification date to be empty", persisted.getLastModification());
assertNull("expected modification user to be empty", persisted.getLastModifiedBy());
assertNotNull("expected version to be non null", persisted.getVersion());
assertTrue("expected version to be greater than 0", persisted.getVersion() > 0L);
}
@Test
public void testUpdateEventIsRegistered() {
assertFalse(repository.exists(KEY));
String expectedCreator = "user1";
String expectedUpdater = "user2";
AuditedItem item = new AuditedItem(KEY, "creation");
auditorAware.setAuditor(expectedCreator);
repository.save(item);
AuditedItem created = repository.findOne(KEY);
auditorAware.setAuditor(expectedUpdater);
repository.save(item);
AuditedItem updated = repository.findOne(KEY);
assertNotNull("expected entity to be persisted", updated);
assertNotNull("expected creation date audit trail", updated.getCreationDate());
assertEquals("expected creation user audit trail", expectedCreator, updated.getCreator());
assertNotNull("expected modification date audit trail", updated.getLastModification());
assertTrue("expected modification date to be after creation date", updated.getCreationDate().before(updated.getLastModification()));
assertEquals("expected modification user to be the modifier", expectedUpdater, updated.getLastModifiedBy());
assertNotNull("expected version to be non null", updated.getVersion());
assertTrue("expected version to be greater than 0", updated.getVersion() > 0L);
assertTrue("expected updated version to be different from the one at creation", created.getVersion() != updated.getVersion());
}
}

View File

@@ -386,3 +386,88 @@ Now you can annotate your fields with JSR303 annotations. If a validation on `sa
private String name;
----
====
[[auditing]]
== Auditing
Entities can be automatically audited (tracing which user created the object, updated the object, and at what times) through Spring Data auditing mechanisms.
First, note that only entities that have a `@Version` annotated field can be audited for creation (otherwise the framework will interpret a creation as an update).
Auditing works by annotating fields with `@CreatedBy`, `@CreatedDate`, `@LastModifiedBy` and `@LastModifiedDate`. The framework will automatically inject the correct values on those fields when persisting the entity. The xxxDate annotations must be put on a `Date` field (or compatible, eg. jodatime classes) while the xxxBy annotations can be put on fields of any class `T` (albeit both fields must be of the same type).
To configure auditing, first you need to have an auditor aware bean in the context. Said bean must be of type `AuditorAware<T>` (allowing to produce a value that can be stored in the xxxBy fields of type `T` we saw earlier). Secondly, you must activate auditing in your `@Configuration` class by using the `@EnableCouchbaseAuditing` annotation.
Here is an example:
.Sample Auditing Entity
====
[source,java]
----
@Document
public class AuditedItem {
@Id
private final String id;
private String value;
@CreatedBy
private String creator;
@LastModifiedBy
private String lastModifiedBy;
@LastModifiedDate
private Date lastModification;
@CreatedDate
private Date creationDate;
@Version
private long version;
//..omitted constructor/getters/setters/...
}
----
====
Notice both `@CreatedBy` and `@LastModifiedBy` are both put on a `String` field, so our `AuditorAware` must work with `String`.
.Sample AuditorAware implementation
====
[source,java]
----
public class NaiveAuditorAware implements AuditorAware<String> {
private String auditor = "auditor";
@Override
public String getCurrentAuditor() {
return auditor;
}
public void setAuditor(String auditor) {
this.auditor = auditor;
}
}
----
====
To tie all that together, we use the java configuration both to declare an AuditorAware bean and to activate auditing:
.Sample Auditing Configuration
====
[source,java]
----
@Configuration
@EnableCouchbaseAuditing //this activates auditing
public class AuditConfiguration extends AbstractCouchbaseConfiguration {
//... a few abstract methods omitted here
// this creates the auditor aware bean that will feed the annotations
@Bean
public NaiveAuditorAware testAuditorAware() {
return new NaiveAuditorAware();
}
----
====

View File

@@ -24,6 +24,7 @@ import com.couchbase.client.java.env.CouchbaseEnvironment;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.support.IsNewStrategyFactory;
/**
* Contains default bean names for Couchbase beans.
@@ -108,4 +109,9 @@ public class BeanNames {
* The name for the bean that registers custom {@link Converter Converters} to encode/decode entity members.
*/
public static final String COUCHBASE_CUSTOM_CONVERSIONS = "couchbaseCustomConversions";
/**
* The name for the bean that will handle audit trail marking of entities.
*/
public static final String COUCHBASE_AUDITING_HANDLER = "couchbaseAuditingHandler";
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2016 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.couchbase.core.mapping.event;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.data.auditing.AuditingHandler;
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.util.Assert;
/**
* Event listener to populate auditing related fields on an entity about to be saved.
*
* @author Oliver Gierke
* @author Simon Baslé
*/
public class AuditingEventListener implements ApplicationListener<BeforeConvertEvent<Object>> {
private final ObjectFactory<IsNewAwareAuditingHandler> auditingHandlerFactory;
/**
* Creates a new {@link AuditingEventListener} using the given {@link MappingContext} and {@link AuditingHandler}
* provided by the given {@link ObjectFactory}.
*
* @param auditingHandlerFactory must not be {@literal null}.
*/
public AuditingEventListener(ObjectFactory<IsNewAwareAuditingHandler> auditingHandlerFactory) {
Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!");
this.auditingHandlerFactory = auditingHandlerFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
*/
public void onApplicationEvent(BeforeConvertEvent<Object> event) {
Object entity = event.getSource();
auditingHandlerFactory.getObject().markAudited(entity);
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2012-2016 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.couchbase.repository.auditing;
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.beans.factory.support.RootBeanDefinition;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.auditing.AuditingHandler;
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport;
import org.springframework.data.auditing.config.AuditingConfiguration;
import org.springframework.data.config.ParsingUtils;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
import org.springframework.data.couchbase.core.mapping.event.AuditingEventListener;
import org.springframework.data.support.IsNewStrategyFactory;
import org.springframework.util.Assert;
/**
* A support registrar that allows to set up auditing for Couchbase (including {@link AuditingHandler}
* and {@link IsNewStrategyFactory} set up). See {@link EnableCouchbaseAuditing} for the associated annotation.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Simon Baslé
*/
public class CouchbaseAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
@Override
protected Class<? extends Annotation> getAnnotation() {
return EnableCouchbaseAuditing.class;
}
@Override
protected String getAuditingHandlerBeanName() {
return BeanNames.COUCHBASE_AUDITING_HANDLER;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!");
Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
ensureMappingContext(registry, annotationMetadata);
super.registerBeanDefinitions(annotationMetadata, registry);
}
@Override
protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) {
Assert.notNull(configuration, "AuditingConfiguration must not be null!");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class);
builder.addConstructorArgReference(BeanNames.COUCHBASE_MAPPING_CONTEXT);
return configureDefaultAuditHandlerAttributes(configuration, builder);
}
@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(AuditingEventListener.class);
listenerBeanDefinitionBuilder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(
getAuditingHandlerBeanName(), registry));
registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(),
AuditingEventListener.class.getName(), registry);
}
private void ensureMappingContext(BeanDefinitionRegistry registry, Object source) {
if (!registry.containsBeanDefinition(BeanNames.COUCHBASE_MAPPING_CONTEXT)) {
RootBeanDefinition definition = new RootBeanDefinition(CouchbaseMappingContext.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
definition.setSource(source);
registry.registerBeanDefinition(BeanNames.COUCHBASE_MAPPING_CONTEXT, definition);
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2012-2016 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.couchbase.repository.auditing;
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 Couchbase via annotation configuration.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Simon Baslé
*/
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(CouchbaseAuditingRegistrar.class)
public @interface EnableCouchbaseAuditing {
/**
* Configures the {@link AuditorAware} bean to be used to lookup the current principal.
*/
String auditorAwareRef() default "";
/**
* Configures whether the creation and modification dates are set. Defaults to {@literal true}.
*/
boolean setDates() default true;
/**
* Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}.
*/
boolean modifyOnCreate() default true;
/**
* Configures a {@link DateTimeProvider} bean name that allows customizing the {@link org.joda.time.DateTime} to be
* used for setting creation and modification dates.
*/
String dateTimeProviderRef() default "";
}