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.
This commit is contained in:
Oliver Gierke
2012-11-13 10:22:36 +08:00
parent 2042bda8a2
commit f1fd8fe0e2
18 changed files with 1586 additions and 0 deletions

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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<Class<?>, AnnotationAuditingMetadata> METADATA_CACHE = new ConcurrentHashMap<Class<?>, AnnotationAuditingMetadata>();
static final List<Class<?>> SUPPORTED_DATE_TYPES;
static {
List<Class<?>> types = new ArrayList<Class<?>>(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;
}
}

View File

@@ -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);
}

View File

@@ -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<Object, ?>) 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<Object, ?> auditable;
public AuditableInterfaceBeanWrapper(Auditable<Object, ?> 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));
}
}
}

View File

@@ -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<T> implements InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(AuditingHandler.class);
private final AuditableBeanWrapperFactory factory = new AuditableBeanWrapperFactory();
private DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE;
private AuditorAware<T> 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<T> 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!");
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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<? extends Annotation> annotationType;
/**
* Creates a new {@link AnnotationFieldFilter} for the given annotation type.
*/
public AnnotationFieldFilter(Class<? extends Annotation> 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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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()));
}
}

View File

@@ -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<AuditedUser, Long> {
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;
}
}

View File

@@ -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<AuditedUser> handler;
AuditorAware<AuditedUser> auditorAware;
AuditedUser user;
@Before
public void setUp() {
handler = new AuditingHandler<AuditedUser>();
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();
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}