From f1fd8fe0e2ca6cf3695e805c82a206a357974b60 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 13 Nov 2012 10:22:36 +0800 Subject: [PATCH] DATACMNS-137, DATAJPA-116 - Added support for annotation based auditing. Introduced annotations CreatedBy, CreatedDate, LastModifiedBy and LastModifiedDate to allow demarcating fields as auditing related ones. The date related annotations require a field type of either DateTime, Date, Long or a primitive long. All these annotations can be used as meta-annotations. Extracted an AuditingHandler from Spring Data JPA which allows to transparently set auditing information on objects implementing Auditable or use the newly introduced annotations in a store independent manner. This allows other stores to implement adapters to provide auditing support as well. Introduced a more convenient way of finding fields in ReflectionUtils using the FieldFilters of core Spring. The API allows to select fields based on a filter and can transparently check that only one field matches the given filter if desired. --- .../data/annotation/CreatedBy.java | 33 +++ .../data/annotation/CreatedDate.java | 33 +++ .../data/annotation/LastModifiedBy.java | 33 +++ .../data/annotation/LastModifiedDate.java | 33 +++ .../auditing/AnnotationAuditingMetadata.java | 139 +++++++++++ .../data/auditing/AuditableBeanWrapper.java | 55 +++++ .../auditing/AuditableBeanWrapperFactory.java | 220 ++++++++++++++++++ .../data/auditing/AuditingHandler.java | 174 ++++++++++++++ .../auditing/CurrentDateTimeProvider.java | 37 +++ .../data/auditing/DateTimeProvider.java | 34 +++ .../data/util/ReflectionUtils.java | 174 ++++++++++++++ .../data/auditing/AnnotatedUser.java | 30 +++ .../AnnotationAuditingMetadataUnitTests.java | 100 ++++++++ .../AuditableBeanWrapperFactoryUnitTests.java | 58 +++++ .../data/auditing/AuditedUser.java | 76 ++++++ .../auditing/AuditingHandlerUnitTests.java | 150 ++++++++++++ ...eflectionAuditingBeanWrapperUnitTests.java | 96 ++++++++ .../data/util/ReflectionUtilsUnitTests.java | 111 +++++++++ 18 files changed, 1586 insertions(+) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedBy.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedDate.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedBy.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedDate.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/auditing/AnnotationAuditingMetadata.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapper.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditingHandler.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/auditing/CurrentDateTimeProvider.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/auditing/DateTimeProvider.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/util/ReflectionUtils.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotatedUser.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotationAuditingMetadataUnitTests.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditableBeanWrapperFactoryUnitTests.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditedUser.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/auditing/ReflectionAuditingBeanWrapperUnitTests.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedBy.java b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedBy.java new file mode 100644 index 000000000..cb0ae37f1 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedBy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a field as the one representing the principal that created the entity containing the field. + * + * @author Ranie Jade Ramiso + * @author Oliver Gierke + * @since 1.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface CreatedBy { +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedDate.java b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedDate.java new file mode 100644 index 000000000..14896d38f --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/CreatedDate.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a field as the one representing the date the entity containing the field was created. + * + * @author Ranie Jade Ramiso + * @author Oliver Gierke + * @since 1.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface CreatedDate { +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedBy.java b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedBy.java new file mode 100644 index 000000000..fdac44919 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedBy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a field as the one representing the principal that recently modified the entity containing the field. + * + * @author Ranie Jade Ramiso + * @author Oliver Gierke + * @since 1.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface LastModifiedBy { +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedDate.java b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedDate.java new file mode 100644 index 000000000..1259bdb60 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/annotation/LastModifiedDate.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a field as the one representing the date the entity containing the field was recently modified. + * + * @author Ranie Jade Ramiso + * @author Oliver Gierke + * @since 1.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface LastModifiedDate { +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AnnotationAuditingMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AnnotationAuditingMetadata.java new file mode 100644 index 000000000..ee98c3f16 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AnnotationAuditingMetadata.java @@ -0,0 +1,139 @@ +package org.springframework.data.auditing; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.joda.time.DateTime; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.data.util.ReflectionUtils.AnnotationFieldFilter; +import org.springframework.util.Assert; + +/** + * Inspects the given {@link Class} for fields annotated by {@link CreatedBy}, {@link CreatedDate}, + * {@link LastModifiedBy} , and {@link LastModifiedDate}. Only one field per annotation is stored. + * + * @author Ranie Jade Ramiso + * @author Oliver Gierke + * @since 1.5 + */ +class AnnotationAuditingMetadata { + + private static final AnnotationFieldFilter CREATED_BY_FILTER = new AnnotationFieldFilter(CreatedBy.class); + private static final AnnotationFieldFilter CREATED_DATE_FILTER = new AnnotationFieldFilter(CreatedDate.class); + private static final AnnotationFieldFilter LAST_MODIFIED_BY_FILTER = new AnnotationFieldFilter(LastModifiedBy.class); + private static final AnnotationFieldFilter LAST_MODIFIED_DATE_FILTER = new AnnotationFieldFilter( + LastModifiedDate.class); + + private static final Map, AnnotationAuditingMetadata> METADATA_CACHE = new ConcurrentHashMap, AnnotationAuditingMetadata>(); + + static final List> SUPPORTED_DATE_TYPES; + + static { + + List> types = new ArrayList>(4); + types.add(DateTime.class); + types.add(Date.class); + types.add(Long.class); + types.add(long.class); + + SUPPORTED_DATE_TYPES = Collections.unmodifiableList(types); + } + + private final Field createdByField; + private final Field createdDateField; + private final Field lastModifiedByField; + private final Field lastModifiedDateField; + + private AnnotationAuditingMetadata(Class type) { + + Assert.notNull(type, "Given type must not be null!"); + + this.createdByField = ReflectionUtils.findField(type, CREATED_BY_FILTER); + this.createdDateField = ReflectionUtils.findField(type, CREATED_DATE_FILTER); + this.lastModifiedByField = ReflectionUtils.findField(type, LAST_MODIFIED_BY_FILTER); + this.lastModifiedDateField = ReflectionUtils.findField(type, LAST_MODIFIED_DATE_FILTER); + + assertValidDateFieldType(createdDateField); + assertValidDateFieldType(lastModifiedDateField); + } + + /** + * Checks whether the given field has a type that is a supported date type. + * + * @param field + */ + private void assertValidDateFieldType(Field field) { + + if (field == null || SUPPORTED_DATE_TYPES.contains(field.getType())) { + return; + } + + throw new IllegalStateException(String.format( + "Found created/modified date field with type %s but only %s are supported!", field.getType(), + SUPPORTED_DATE_TYPES)); + } + + /** + * Return a {@link AnnotationAuditingMetadata} for the given {@link Class}. + * + * @param type the type to inspect, must not be {@literal null}. + */ + public static AnnotationAuditingMetadata getMetadata(Class type) { + + if (METADATA_CACHE.containsKey(type)) { + return METADATA_CACHE.get(type); + } + + AnnotationAuditingMetadata metadata = new AnnotationAuditingMetadata(type); + METADATA_CACHE.put(type, metadata); + return metadata; + } + + /** + * Returns whether the {@link Class} represented in this instance is auditable or not. + */ + public boolean isAuditable() { + if (createdByField == null && createdDateField == null && lastModifiedByField == null + && lastModifiedDateField == null) { + return false; + } + return true; + } + + /** + * Return the field annotated by {@link CreatedBy}, or {@literal null}. + */ + public Field getCreatedByField() { + return createdByField; + } + + /** + * Return the field annotated by {@link CreatedDate}, or {@literal null}. + */ + public Field getCreatedDateField() { + return createdDateField; + } + + /** + * Return the field annotated by {@link LastModifiedBy}, or {@literal null}. + */ + public Field getLastModifiedByField() { + return lastModifiedByField; + } + + /** + * Return the field annotated by {@link LastModifiedDate}, or {@literal null}. + */ + public Field getLastModifiedDateField() { + return lastModifiedDateField; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapper.java b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapper.java new file mode 100644 index 000000000..3223dcdd5 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapper.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012 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.auditing; + +import org.joda.time.DateTime; + +/** + * Interface to abstract the ways setting the auditing information can be implemented. + * + * @author Oliver Gierke + * @since 1.5 + */ +public interface AuditableBeanWrapper { + + /** + * Set the creator of the object. + * + * @param value + */ + void setCreatedBy(Object value); + + /** + * Set the date the object was created. + * + * @param value + */ + void setCreatedDate(DateTime value); + + /** + * Set the last modifier of the object. + * + * @param value + */ + void setLastModifiedBy(Object value); + + /** + * Set the last modification date. + * + * @param value + */ + void setLastModifiedDate(DateTime value); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java new file mode 100644 index 000000000..5d25e7a97 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java @@ -0,0 +1,220 @@ +/* + * Copyright 2012 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.auditing; + +import java.lang.reflect.Field; +import java.util.Date; + +import org.joda.time.DateTime; +import org.springframework.data.domain.Auditable; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.util.Assert; + +/** + * A factory class to {@link AuditableBeanWrapper} instances. + * + * @author Oliver Gierke + * @since 1.5 + */ +class AuditableBeanWrapperFactory { + + /** + * Returns an {@link AuditableBeanWrapper} if the given object is capable of being equipped with auditing information. + * + * @param source the auditing candidate. + * @return + */ + @SuppressWarnings("unchecked") + public AuditableBeanWrapper getBeanWrapperFor(Object source) { + + if (source == null) { + return null; + } + + if (source instanceof Auditable) { + return new AuditableInterfaceBeanWrapper((Auditable) source); + } + + AnnotationAuditingMetadata metadata = AnnotationAuditingMetadata.getMetadata(source.getClass()); + + if (metadata.isAuditable()) { + return new ReflectionAuditingBeanWrapper(source); + } + + return null; + } + + /** + * An {@link AuditableBeanWrapper} that works with objects implementing + * + * @author Oliver Gierke + */ + static class AuditableInterfaceBeanWrapper implements AuditableBeanWrapper { + + private final Auditable auditable; + + public AuditableInterfaceBeanWrapper(Auditable auditable) { + this.auditable = auditable; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedBy(java.lang.Object) + */ + public void setCreatedBy(Object value) { + auditable.setCreatedBy(value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedDate(org.joda.time.DateTime) + */ + public void setCreatedDate(DateTime value) { + auditable.setCreatedDate(value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedBy(java.lang.Object) + */ + public void setLastModifiedBy(Object value) { + auditable.setLastModifiedBy(value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedDate(org.joda.time.DateTime) + */ + public void setLastModifiedDate(DateTime value) { + auditable.setLastModifiedDate(value); + } + } + + /** + * An {@link AuditableBeanWrapper} implementation that sets values on the target object using refelction. + * + * @author Oliver Gierke + */ + static class ReflectionAuditingBeanWrapper implements AuditableBeanWrapper { + + private final AnnotationAuditingMetadata metadata; + private final Object target; + + /** + * Creates a new {@link ReflectionAuditingBeanWrapper} to set auditing data on the given target object. + * + * @param target must not be {@literal null}. + */ + public ReflectionAuditingBeanWrapper(Object target) { + + Assert.notNull(target, "Target object must not be null!"); + + this.metadata = AnnotationAuditingMetadata.getMetadata(target.getClass()); + this.target = target; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedBy(java.lang.Object) + */ + public void setCreatedBy(Object value) { + setField(metadata.getCreatedByField(), value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedDate(org.joda.time.DateTime) + */ + public void setCreatedDate(DateTime value) { + setDateField(metadata.getCreatedDateField(), value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedBy(java.lang.Object) + */ + public void setLastModifiedBy(Object value) { + setField(metadata.getLastModifiedByField(), value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedDate(org.joda.time.DateTime) + */ + public void setLastModifiedDate(DateTime value) { + setDateField(metadata.getLastModifiedDateField(), value); + } + + /** + * Sets the given field to the given value if the field is not {@literal null}. + * + * @param field + * @param value + */ + private void setField(Field field, Object value) { + + if (field != null) { + ReflectionUtils.setField(field, target, value); + } + } + + /** + * Sets the given field to the given value if the field is not {@literal null}. + * + * @param field + * @param value + */ + private void setDateField(Field field, DateTime value) { + + if (field == null) { + return; + } + + ReflectionUtils.setField(field, target, getDateValueToSet(value, field)); + } + + /** + * Returns the {@link DateTime} in a type compatible to the given field. + * + * @param value + * @param field must not be {@literal null}. + * @return + */ + private Object getDateValueToSet(DateTime value, Field field) { + + if (value == null) { + return null; + } + + Class targetType = field.getType(); + + if (DateTime.class.equals(targetType)) { + return value; + } + + if (Date.class.equals(targetType)) { + return value.toDate(); + } + + if (Long.class.equals(targetType) || long.class.equals(targetType)) { + return value.getMillis(); + } + + throw new IllegalArgumentException(String.format("Invalid date type for field %s! Supported types are %s.", + field, AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES)); + } + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditingHandler.java b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditingHandler.java new file mode 100644 index 000000000..e91ba9bc3 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/AuditingHandler.java @@ -0,0 +1,174 @@ +/* + * Copyright 2012 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.auditing; + +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.domain.Auditable; +import org.springframework.data.domain.AuditorAware; +import org.springframework.util.Assert; + +/** + * Auditing handler to mark entity objects created and modified. + * + * @author Oliver Gierke + * @since 1.5 + */ +public class AuditingHandler implements InitializingBean { + + private static final Logger LOGGER = LoggerFactory.getLogger(AuditingHandler.class); + + private final AuditableBeanWrapperFactory factory = new AuditableBeanWrapperFactory(); + private DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE; + private AuditorAware auditorAware; + private boolean dateTimeForNow = true; + private boolean modifyOnCreation = true; + + /** + * Setter to inject a {@code AuditorAware} component to retrieve the current auditor. + * + * @param auditorAware the auditorAware to set + */ + public void setAuditorAware(final AuditorAware auditorAware) { + + Assert.notNull(auditorAware); + this.auditorAware = auditorAware; + } + + /** + * Setter do determine if {@link Auditable#setCreatedDate(DateTime)} and + * {@link Auditable#setLastModifiedDate(DateTime)} shall be filled with the current Java time. Defaults to + * {@code true}. One might set this to {@code false} to use database features to set entity time. + * + * @param dateTimeForNow the dateTimeForNow to set + */ + public void setDateTimeForNow(boolean dateTimeForNow) { + this.dateTimeForNow = dateTimeForNow; + } + + /** + * Set this to false if you want to treat entity creation as modification and thus set the current date as + * modification date, too. Defaults to {@code true}. + * + * @param modifyOnCreation if modification information shall be set on creation, too + */ + public void setModifyOnCreation(final boolean modifyOnCreation) { + this.modifyOnCreation = modifyOnCreation; + } + + /** + * Sets the {@link DateTimeProvider} to be used to determine the dates to be set. + * + * @param dateTimeProvider + */ + public void setDateTimeProvider(DateTimeProvider dateTimeProvider) { + this.dateTimeProvider = dateTimeProvider == null ? CurrentDateTimeProvider.INSTANCE : dateTimeProvider; + } + + /** + * Marks the given object as created. + * + * @param source + */ + public void markCreated(Object source) { + touch(source, true); + } + + /** + * Marks the given object as modified. + * + * @param source + */ + public void markModified(Object source) { + touch(source, false); + } + + private void touch(Object target, boolean isNew) { + + AuditableBeanWrapper wrapper = factory.getBeanWrapperFor(target); + + if (wrapper == null) { + return; + } + + T auditor = touchAuditor(wrapper, isNew); + DateTime now = dateTimeForNow ? touchDate(wrapper, isNew) : null; + + Object defaultedNow = now == null ? "not set" : now; + Object defaultedAuditor = auditor == null ? "unknown" : auditor; + + LOGGER.debug("Touched {} - Last modification at {} by {}", new Object[] { target, defaultedNow, defaultedAuditor }); + } + + /** + * Sets modifying and creating auditioner. Creating auditioner is only set on new auditables. + * + * @param auditable + * @return + */ + private T touchAuditor(AuditableBeanWrapper wrapper, boolean isNew) { + + if (null == auditorAware) { + return null; + } + + T auditor = auditorAware.getCurrentAuditor(); + + if (isNew) { + wrapper.setCreatedBy(auditor); + if (!modifyOnCreation) { + return auditor; + } + } + + wrapper.setLastModifiedBy(auditor); + return auditor; + } + + /** + * Touches the auditable regarding modification and creation date. Creation date is only set on new auditables. + * + * @param wrapper + * @return + */ + private DateTime touchDate(AuditableBeanWrapper wrapper, boolean isNew) { + + DateTime now = dateTimeProvider.getDateTime(); + + if (isNew) { + wrapper.setCreatedDate(now); + if (!modifyOnCreation) { + return now; + } + } + + wrapper.setLastModifiedDate(now); + return now; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() { + + if (auditorAware == null) { + LOGGER.debug("No AuditorAware set! Auditing will not be applied!"); + } + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/auditing/CurrentDateTimeProvider.java b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/CurrentDateTimeProvider.java new file mode 100644 index 000000000..c8bbed8d8 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/CurrentDateTimeProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012 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.auditing; + +import org.joda.time.DateTime; + +/** + * Default {@link DateTimeProvider} simply creating new {@link DateTime} instances for each method call. + * + * @author Oliver Gierke + * @since 1.5 + */ +public enum CurrentDateTimeProvider implements DateTimeProvider { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.domain.support.DateTimeProvider#getDateTime() + */ + public DateTime getDateTime() { + return new DateTime(); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/auditing/DateTimeProvider.java b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/DateTimeProvider.java new file mode 100644 index 000000000..1b4a7d5a4 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/auditing/DateTimeProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012 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.auditing; + +import org.joda.time.DateTime; + +/** + * SPI to calculate the {@link DateTime} instance to be used when auditing. + * + * @author Oliver Gierke + * @since 1.5 + */ +public interface DateTimeProvider { + + /** + * Returns the {@link DateTime} to be used as modification date. + * + * @return + */ + DateTime getDateTime(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/ReflectionUtils.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/ReflectionUtils.java new file mode 100644 index 000000000..1e3c916b0 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -0,0 +1,174 @@ +/* + * Copyright 2012 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.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils.FieldFilter; + +/** + * Spring Data specific reflection utility methods and classes. + * + * @author Oliver Gierke + * @since 1.5 + */ +public class ReflectionUtils { + + /** + * A {@link FieldFilter} that has a description. + * + * @author Oliver Gierke + */ + public interface DescribedFieldFilter extends FieldFilter { + + /** + * Returns the description of the field filter. Used in exceptions being thrown in case uniqueness shall be enforced + * on the field filter. + * + * @return + */ + String getDescription(); + } + + /** + * A {@link FieldFilter} for a given annotation. + * + * @author Oliver Gierke + */ + public static class AnnotationFieldFilter implements DescribedFieldFilter { + + private final Class annotationType; + + /** + * Creates a new {@link AnnotationFieldFilter} for the given annotation type. + */ + public AnnotationFieldFilter(Class annotationType) { + + Assert.notNull(annotationType, "Annotation type must not be null!"); + this.annotationType = annotationType; + } + + /* + * (non-Javadoc) + * @see org.springframework.util.ReflectionUtils.FieldFilter#matches(java.lang.reflect.Field) + */ + public boolean matches(Field field) { + return AnnotationUtils.getAnnotation(field, annotationType) != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.ReflectionUtils.DescribedFieldFilter#getDescription() + */ + public String getDescription() { + return String.format("Annotation filter for %s", annotationType.getName()); + } + } + + /** + * Finds the first field on the given class matching the given {@link FieldFilter}. + * + * @param type must not be {@literal null}. + * @param filter must not be {@literal null}. + * @return the field matching the filter or {@literal null} in case no field could be found. + */ + public static Field findField(Class type, final FieldFilter filter) { + + return findField(type, new DescribedFieldFilter() { + + public boolean matches(Field field) { + return filter.matches(field); + } + + public String getDescription() { + return String.format("FieldFilter %s", filter.toString()); + } + }, false); + } + + /** + * Finds the field matching the given {@link DescribedFieldFilter}. Will make sure there's only one field matching the + * filter. + * + * @see #findField(Class, DescribedFieldFilter, boolean) + * @param type must not be {@literal null}. + * @param filter must not be {@literal null}. + * @return the field matching the given {@link DescribedFieldFilter} or {@literal null} if none found. + * @throws IllegalStateException in case more than one matching field is found + */ + public static Field findField(Class type, DescribedFieldFilter filter) { + return findField(type, filter, true); + } + + /** + * Finds the field matching the given {@link DescribedFieldFilter}. Will make sure there's only one field matching the + * filter in case {@code enforceUniqueness} is {@literal true}. + * + * @param type must not be {@literal null}. + * @param filter must not be {@literal null}. + * @param enforceUniqueness whether to enforce uniqueness of the field + * @return the field matching the given {@link DescribedFieldFilter} or {@literal null} if none found. + * @throws IllegalStateException if enforceUniqueness is true and more than one matching field is found + */ + public static Field findField(Class type, DescribedFieldFilter filter, boolean enforceUniqueness) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(filter, "Filter must not be null!"); + + Class targetClass = type; + Field foundField = null; + + while (targetClass != Object.class) { + + for (Field field : targetClass.getDeclaredFields()) { + + if (!filter.matches(field)) { + continue; + } + + if (!enforceUniqueness) { + return field; + } + + if (foundField != null && enforceUniqueness) { + throw new IllegalStateException(filter.getDescription()); + } + + foundField = field; + } + + targetClass = targetClass.getSuperclass(); + } + + return foundField; + } + + /** + * Sets the given field on the given object to the given value. Will make sure the given field is accessible. + * + * @param field must not be {@literal null}. + * @param target must not be {@literal null}. + * @param value + */ + public static void setField(Field field, Object target, Object value) { + + org.springframework.util.ReflectionUtils.makeAccessible(field); + org.springframework.util.ReflectionUtils.setField(field, target, value); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotatedUser.java b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotatedUser.java new file mode 100644 index 000000000..4002eff6b --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotatedUser.java @@ -0,0 +1,30 @@ +package org.springframework.data.auditing; + +import java.util.Date; + +import org.joda.time.DateTime; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; + +/** + * Sample entity using annotation based auditing. + * + * @author Oliver Gierke + * @since 1.5 + */ +class AnnotatedUser { + + @CreatedBy + Object createdBy; + + @CreatedDate + DateTime createdDate; + + @LastModifiedBy + Object lastModifiedBy; + + @LastModifiedDate + Date lastModifiedDate; +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotationAuditingMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotationAuditingMetadataUnitTests.java new file mode 100644 index 000000000..f36dd7f92 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AnnotationAuditingMetadataUnitTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012 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.auditing; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Field; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.util.ReflectionUtils; + +/** + * Unit test for {@link org.springframework.data.auditing.AnnotationAuditingMetadata}. + * + * @author Ranie Jade Ramiso + * @author Oliver Gierke + * @since 1.5 + */ +public class AnnotationAuditingMetadataUnitTests { + + static final Field createdByField = ReflectionUtils.findField(AnnotatedUser.class, "createdBy"); + static final Field createdDateField = ReflectionUtils.findField(AnnotatedUser.class, "createdDate"); + static final Field lastModifiedByField = ReflectionUtils.findField(AnnotatedUser.class, "lastModifiedBy"); + static final Field lastModifiedDateField = ReflectionUtils.findField(AnnotatedUser.class, "lastModifiedDate"); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void checkAnnotationDiscovery() { + + AnnotationAuditingMetadata metadata = AnnotationAuditingMetadata.getMetadata(AnnotatedUser.class); + assertThat(metadata, is(notNullValue())); + + assertThat(createdByField, is(metadata.getCreatedByField())); + assertThat(createdDateField, is(metadata.getCreatedDateField())); + assertThat(lastModifiedByField, is(metadata.getLastModifiedByField())); + assertThat(lastModifiedDateField, is(metadata.getLastModifiedDateField())); + } + + @Test + public void checkCaching() { + + AnnotationAuditingMetadata firstCall = AnnotationAuditingMetadata.getMetadata(AnnotatedUser.class); + assertThat(firstCall, is(notNullValue())); + + AnnotationAuditingMetadata secondCall = AnnotationAuditingMetadata.getMetadata(AnnotatedUser.class); + assertThat(firstCall, is(secondCall)); + } + + @Test + public void checkIsAuditable() { + + AnnotationAuditingMetadata metadata = AnnotationAuditingMetadata.getMetadata(AnnotatedUser.class); + assertThat(metadata, is(notNullValue())); + ; + assertThat(metadata.isAuditable(), is(true)); + + metadata = AnnotationAuditingMetadata.getMetadata(NonAuditableUser.class); + assertThat(metadata, is(notNullValue())); + assertThat(metadata.isAuditable(), is(false)); + } + + @Test + public void rejectsInvalidDateTypeField() { + + class Sample { + @CreatedDate + String field; + } + + exception.expect(IllegalStateException.class); + exception.expectMessage(String.class.getName()); + exception.expectMessage("field"); + + AnnotationAuditingMetadata.getMetadata(Sample.class); + } + + @SuppressWarnings("unused") + static class NonAuditableUser { + private Object nonAuditProperty; + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditableBeanWrapperFactoryUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditableBeanWrapperFactoryUnitTests.java new file mode 100644 index 000000000..4160eff96 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditableBeanWrapperFactoryUnitTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012 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.auditing; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.auditing.AuditableBeanWrapperFactory.AuditableInterfaceBeanWrapper; +import org.springframework.data.auditing.AuditableBeanWrapperFactory.ReflectionAuditingBeanWrapper; + +/** + * @author Oliver Gierke + * @since 1.5 + */ +public class AuditableBeanWrapperFactoryUnitTests { + + AuditableBeanWrapperFactory factory = new AuditableBeanWrapperFactory(); + + @Test + public void returnsNullForNullSource() { + assertThat(factory.getBeanWrapperFor(null), is(nullValue())); + } + + @Test + public void returnsAuditableInterfaceBeanWrapperForAuditable() { + + AuditableBeanWrapper wrapper = factory.getBeanWrapperFor(new AuditedUser()); + assertThat(wrapper, is(instanceOf(AuditableInterfaceBeanWrapper.class))); + } + + @Test + public void returnsReflectionAuditingBeanWrapperForNonAuditableButAnnotated() { + + AuditableBeanWrapper wrapper = factory.getBeanWrapperFor(new AnnotatedUser()); + assertThat(wrapper, is(instanceOf(ReflectionAuditingBeanWrapper.class))); + } + + @Test + public void returnsNullForNonAuditableType() { + + AuditableBeanWrapper wrapper = factory.getBeanWrapperFor(new Object()); + assertThat(wrapper, is(nullValue())); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditedUser.java b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditedUser.java new file mode 100644 index 000000000..152ea5e23 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditedUser.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012 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.auditing; + +import org.joda.time.DateTime; +import org.springframework.data.domain.Auditable; + +/** + * Sample implementation of {@link Auditable}. + * + * @author Oliver Gierke + * @since 1.5 + */ +class AuditedUser implements Auditable { + + private static final long serialVersionUID = -840865084027597951L; + + Long id; + AuditedUser createdBy; + AuditedUser modifiedBy; + DateTime createdDate; + DateTime modifiedDate; + + public Long getId() { + return id; + } + + public boolean isNew() { + return id == null; + } + + public AuditedUser getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(AuditedUser createdBy) { + this.createdBy = createdBy; + } + + public DateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(DateTime creationDate) { + this.createdDate = creationDate; + } + + public AuditedUser getLastModifiedBy() { + return modifiedBy; + } + + public void setLastModifiedBy(AuditedUser lastModifiedBy) { + this.modifiedBy = lastModifiedBy; + } + + public DateTime getLastModifiedDate() { + return modifiedDate; + } + + public void setLastModifiedDate(DateTime lastModifiedDate) { + this.modifiedDate = lastModifiedDate; + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java new file mode 100644 index 000000000..6017300b7 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2008-2012 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.auditing; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.AuditorAware; + +/** + * Unit test for {@code AuditingHandler}. + * + * @author Oliver Gierke + * @since 1.5 + */ +@SuppressWarnings("unchecked") +public class AuditingHandlerUnitTests { + + AuditingHandler handler; + AuditorAware auditorAware; + + AuditedUser user; + + @Before + public void setUp() { + + handler = new AuditingHandler(); + user = new AuditedUser(); + + auditorAware = mock(AuditorAware.class); + when(auditorAware.getCurrentAuditor()).thenReturn(user); + } + + /** + * Checks that the advice does not set auditor on the target entity if no {@code AuditorAware} was configured. + */ + @Test + public void doesNotSetAuditorIfNotConfigured() { + + handler.markCreated(user); + + assertNotNull(user.getCreatedDate()); + assertNotNull(user.getLastModifiedDate()); + + assertNull(user.getCreatedBy()); + assertNull(user.getLastModifiedBy()); + } + + /** + * Checks that the advice sets the auditor on the target entity if an {@code AuditorAware} was configured. + */ + @Test + public void setsAuditorIfConfigured() { + + handler.setAuditorAware(auditorAware); + + handler.markCreated(user); + + assertNotNull(user.getCreatedDate()); + assertNotNull(user.getLastModifiedDate()); + + assertNotNull(user.getCreatedBy()); + assertNotNull(user.getLastModifiedBy()); + + verify(auditorAware).getCurrentAuditor(); + } + + /** + * Checks that the advice does not set modification information on creation if the falg is set to {@code false}. + */ + @Test + public void honoursModifiedOnCreationFlag() { + + handler.setAuditorAware(auditorAware); + handler.setModifyOnCreation(false); + handler.markCreated(user); + + assertNotNull(user.getCreatedDate()); + assertNotNull(user.getCreatedBy()); + + assertNull(user.getLastModifiedBy()); + assertNull(user.getLastModifiedDate()); + + verify(auditorAware).getCurrentAuditor(); + } + + /** + * Tests that the advice only sets modification data if a not-new entity is handled. + */ + @Test + public void onlySetsModificationDataOnNotNewEntities() { + + user = new AuditedUser(); + user.id = 1L; + + handler.setAuditorAware(auditorAware); + handler.markModified(user); + + assertNull(user.getCreatedBy()); + assertNull(user.getCreatedDate()); + + assertNotNull(user.getLastModifiedBy()); + assertNotNull(user.getLastModifiedDate()); + + verify(auditorAware).getCurrentAuditor(); + } + + @Test + public void doesNotSetTimeIfConfigured() { + + handler.setDateTimeForNow(false); + handler.setAuditorAware(auditorAware); + handler.markCreated(user); + + assertNotNull(user.getCreatedBy()); + assertNull(user.getCreatedDate()); + + assertNotNull(user.getLastModifiedBy()); + assertNull(user.getLastModifiedDate()); + } + + /** + * @see DATAJPA-9 + */ + @Test + public void usesDateTimeProviderIfConfigured() { + + DateTimeProvider provider = mock(DateTimeProvider.class); + + handler.setDateTimeProvider(provider); + handler.markCreated(user); + + verify(provider, times(1)).getDateTime(); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/auditing/ReflectionAuditingBeanWrapperUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/ReflectionAuditingBeanWrapperUnitTests.java new file mode 100644 index 000000000..083b17a0d --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/auditing/ReflectionAuditingBeanWrapperUnitTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012 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.auditing; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.auditing.AuditableBeanWrapperFactory.ReflectionAuditingBeanWrapper; + +/** + * Unit tests for {@link ReflectionAuditingBeanWrapper}. + * + * @author Oliver Gierke + * @since 1.5 + */ +public class ReflectionAuditingBeanWrapperUnitTests { + + AnnotationAuditingMetadata metadata; + AnnotatedUser user; + AuditableBeanWrapper wrapper; + + DateTime time = new DateTime(); + + @Before + public void setUp() { + + this.user = new AnnotatedUser(); + this.wrapper = new ReflectionAuditingBeanWrapper(user); + } + + @Test + public void setsDateTimeFieldCorrectly() { + + wrapper.setCreatedDate(time); + assertThat(user.createdDate, is(time)); + } + + @Test + public void setsDateFieldCorrectly() { + + wrapper.setLastModifiedDate(time); + assertThat(user.lastModifiedDate, is(time.toDate())); + } + + @Test + public void setsLongFieldCorrectly() { + + class Sample { + + @CreatedDate + Long createdDate; + + @LastModifiedDate + long modifiedDate; + } + + Sample sample = new Sample(); + AuditableBeanWrapper wrapper = new ReflectionAuditingBeanWrapper(sample); + + wrapper.setCreatedDate(time); + assertThat(sample.createdDate, is(time.getMillis())); + + wrapper.setLastModifiedDate(time); + assertThat(sample.modifiedDate, is(time.getMillis())); + } + + @Test + public void setsAuditorFieldsCorrectly() { + + Object object = new Object(); + + wrapper.setCreatedBy(object); + assertThat(user.createdBy, is(object)); + + wrapper.setLastModifiedBy(object); + assertThat(user.lastModifiedBy, is(object)); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java new file mode 100644 index 000000000..b39ef1288 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012 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.util; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Field; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.ReflectionUtils.DescribedFieldFilter; +import org.springframework.util.ReflectionUtils.FieldFilter; + +/** + * @author Oliver Gierke + */ +public class ReflectionUtilsUnitTests { + + Field reference; + + @Before + public void setUp() throws Exception { + this.reference = Sample.class.getField("field"); + } + + @Test + public void findsFieldByFilter() { + + Field field = ReflectionUtils.findField(Sample.class, (FieldFilter) new FieldNameFieldFilter("field")); + assertThat(field, is(reference)); + } + + @Test + public void returnsNullIfNoFieldFound() { + + Field field = ReflectionUtils.findField(Sample.class, (FieldFilter) new FieldNameFieldFilter("foo")); + assertThat(field, is(nullValue())); + } + + @Test(expected = IllegalStateException.class) + public void rejectsNonUniqueField() { + ReflectionUtils.findField(Sample.class, new ReflectionUtils.AnnotationFieldFilter(Autowired.class)); + } + + @Test + public void findsUniqueField() { + + Field field = ReflectionUtils.findField(Sample.class, new FieldNameFieldFilter("field"), false); + assertThat(field, is(reference)); + } + + @Test + public void findsFieldInSuperclass() { + + class Subclass extends Sample { + + } + + Field field = ReflectionUtils.findField(Subclass.class, new FieldNameFieldFilter("field")); + assertThat(field, is(reference)); + } + + @Test + public void setsNonPublicField() { + + Sample sample = new Sample(); + Field field = ReflectionUtils.findField(Sample.class, new FieldNameFieldFilter("first")); + ReflectionUtils.setField(field, sample, "foo"); + assertThat(sample.first, is("foo")); + } + + static class Sample { + + public String field; + + @Autowired + String first, second; + } + + static class FieldNameFieldFilter implements DescribedFieldFilter { + + private final String name; + + public FieldNameFieldFilter(String name) { + this.name = name; + } + + public boolean matches(Field field) { + return field.getName().equals(name); + } + + public String getDescription() { + return String.format("Filter for fields named %s", name); + } + } +}