From 39fb52d2a656990ac5af0c7770687bcdb9b73afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 23 Feb 2016 18:58:29 +0100 Subject: [PATCH] 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. --- .../auditing/AuditedApplicationConfig.java | 17 +++ .../auditing/AuditedAuditorAware.java | 18 +++ .../repository/auditing/AuditedItem.java | 129 ++++++++++++++++++ .../auditing/AuditedRepository.java | 7 + .../repository/auditing/AuditingTests.java | 89 ++++++++++++ src/main/asciidoc/entity.adoc | 85 ++++++++++++ .../data/couchbase/config/BeanNames.java | 6 + .../mapping/event/AuditingEventListener.java | 56 ++++++++ .../auditing/CouchbaseAuditingRegistrar.java | 99 ++++++++++++++ .../auditing/EnableCouchbaseAuditing.java | 63 +++++++++ 10 files changed, 569 insertions(+) create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedApplicationConfig.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedAuditorAware.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedItem.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedRepository.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditingTests.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java diff --git a/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedApplicationConfig.java b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedApplicationConfig.java new file mode 100644 index 00000000..7252a4d1 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedApplicationConfig.java @@ -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(); + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedAuditorAware.java b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedAuditorAware.java new file mode 100644 index 00000000..07812561 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedAuditorAware.java @@ -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 { + + private String auditor = "auditor"; + + @Override + public String getCurrentAuditor() { + return auditor; + } + + public void setAuditor(String auditor) { + this.auditor = auditor; + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedItem.java b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedItem.java new file mode 100644 index 00000000..6b347631 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedItem.java @@ -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; + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedRepository.java new file mode 100644 index 00000000..b9c87d54 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditedRepository.java @@ -0,0 +1,7 @@ +package org.springframework.data.couchbase.repository.auditing; + +import org.springframework.data.couchbase.repository.CouchbaseRepository; + +public interface AuditedRepository extends CouchbaseRepository { + +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditingTests.java b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditingTests.java new file mode 100644 index 00000000..22d63e04 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/auditing/AuditingTests.java @@ -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()); + } +} diff --git a/src/main/asciidoc/entity.adoc b/src/main/asciidoc/entity.adoc index cdca22b3..8b5d08f4 100644 --- a/src/main/asciidoc/entity.adoc +++ b/src/main/asciidoc/entity.adoc @@ -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` (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 { + + 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(); + } +---- +==== \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index 5b1ceaff..2b5591dc 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -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"; } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java new file mode 100644 index 00000000..abe5cf0e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java @@ -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> { + + private final ObjectFactory 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 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 event) { + + Object entity = event.getSource(); + auditingHandlerFactory.getObject().markAudited(entity); + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java new file mode 100644 index 00000000..5c7107e0 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java @@ -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 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); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java new file mode 100644 index 00000000..49e6eb32 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java @@ -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 ""; +}