DATACMNS-365 - Enhanced auditing subsystem to work with accessor annotations.

Added a MappingAuditableBeanWrapperFactory to be able to use themapping metamodel to lookup annotations on persistent properties. This propagates into AuditingHandler and IsNewAwareAuditingHandler getting new constructors taking a MappingContext to set themselves up correctly. This will probably need store specific updates in the setup of the auditing infrastructure for namespace implementations and annotation based JavaConfig.

Introduced ….getPersistentProperty(Class<? extends Annotation> annotationType) on PersistentEntity to be able to access properties with a given annotation.

Updated SonarGraph architecture description and moved auditing related config classes into auditing.config package.
This commit is contained in:
Oliver Gierke
2014-03-18 15:52:19 +01:00
parent 0e7a444aba
commit e9bcca11be
14 changed files with 680 additions and 158 deletions

View File

@@ -99,6 +99,20 @@
<dependency toName="Project|spring-data-commons::Layer|Core" type="AllowedDependency"/>
<dependency toName="Project|spring-data-commons::Layer|Mapping" type="AllowedDependency"/>
</element>
<element type="Layer" name="Auditing">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.auditing.**"/>
</element>
<element type="Subsystem" name="Configuration">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.config.**"/>
</element>
</element>
<dependency toName="External|External" type="AllowedDependency"/>
<dependency toName="Project|spring-data-commons::Layer|Application" type="AllowedDependency"/>
<dependency toName="Project|spring-data-commons::Layer|Core" type="AllowedDependency"/>
<dependency toName="Project|spring-data-commons::Layer|Mapping" type="AllowedDependency"/>
</element>
<element type="Layer" name="Mapping">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.mapping.**"/>
@@ -135,12 +149,6 @@
<element type="IncludeTypePattern" name="**.authentication.**"/>
</element>
</element>
<element type="Subsystem" name="Configuration">
<element type="TypeFilter" name="Assignment">
<element type="WeakTypePattern" name="**.config.**"/>
</element>
<stereotype name="Unrestricted"/>
</element>
<element type="Subsystem" name="History">
<element type="TypeFilter" name="Assignment">
<element type="WeakTypePattern" name="**.history.**"/>
@@ -151,18 +159,24 @@
<element type="WeakTypePattern" name="**.support.**"/>
</element>
</element>
<element type="Subsystem" name="Auditing">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.auditing.**"/>
</element>
<stereotype name="Unrestricted"/>
</element>
<element type="Subsystem" name="Transactions">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.transaction.**"/>
</element>
<stereotype name="Unrestricted"/>
</element>
<element type="Subsystem" name="Configuration">
<element type="TypeFilter" name="Assignment">
<element type="WeakTypePattern" name="**.config.**"/>
</element>
<stereotype name="Unrestricted"/>
</element>
<element type="Subsystem" name="Geo">
<element type="TypeFilter" name="Assignment">
<element type="IncludeTypePattern" name="**.geo.**"/>
</element>
<stereotype name="Unrestricted"/>
</element>
<dependency toName="Project|spring-data-commons::Layer|Application" type="AllowedDependency"/>
</element>
<element type="Layer" name="Application">

View File

@@ -22,7 +22,6 @@ import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.data.domain.Auditable;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.format.support.DefaultFormattingConversionService;
@@ -37,9 +36,6 @@ import org.springframework.util.ClassUtils;
*/
class AuditableBeanWrapperFactory {
private static boolean IS_JODA_TIME_PRESENT = ClassUtils.isPresent("org.joda.time.DateTime",
ReflectionAuditingBeanWrapper.class.getClassLoader());
/**
* Returns an {@link AuditableBeanWrapper} if the given object is capable of being equipped with auditing information.
*
@@ -112,14 +108,69 @@ class AuditableBeanWrapperFactory {
}
}
/**
* Base class for {@link AuditableBeanWrapper} implementations that might need to convert {@link Calendar} values into
* compatible types when setting date/time information.
*
* @author Oliver Gierke
* @since 1.8
*/
static abstract class DateConvertingAuditableBeanWrapper implements AuditableBeanWrapper {
private static boolean IS_JODA_TIME_PRESENT = ClassUtils.isPresent("org.joda.time.DateTime",
ReflectionAuditingBeanWrapper.class.getClassLoader());
private final ConversionService conversionService;
/**
* Creates a new {@link DateConvertingAuditableBeanWrapper}.
*/
public DateConvertingAuditableBeanWrapper() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
if (IS_JODA_TIME_PRESENT) {
conversionService.addConverter(CalendarToDateTimeConverter.INSTANCE);
conversionService.addConverter(CalendarToLocalDateTimeConverter.INSTANCE);
}
this.conversionService = conversionService;
}
/**
* Returns the {@link Calendar} in a type, compatible to the given field.
*
* @param value can be {@literal null}.
* @param targetType must not be {@literal null}.
* @param source must not be {@literal null}.
* @return
*/
protected Object getDateValueToSet(Calendar value, Class<?> targetType, Object source) {
if (value == null) {
return null;
}
if (Calendar.class.equals(targetType)) {
return value;
}
if (conversionService.canConvert(Calendar.class, targetType)) {
return conversionService.convert(value, targetType);
}
throw new IllegalArgumentException(String.format("Invalid date type for member %s! Supported types are %s.",
source, AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES));
}
}
/**
* An {@link AuditableBeanWrapper} implementation that sets values on the target object using refelction.
*
* @author Oliver Gierke
*/
static class ReflectionAuditingBeanWrapper implements AuditableBeanWrapper {
static class ReflectionAuditingBeanWrapper extends DateConvertingAuditableBeanWrapper {
private final ConversionService conversionService;
private final AnnotationAuditingMetadata metadata;
private final Object target;
@@ -134,15 +185,6 @@ class AuditableBeanWrapperFactory {
this.metadata = AnnotationAuditingMetadata.getMetadata(target.getClass());
this.target = target;
ConfigurableConversionService conversionService = new DefaultFormattingConversionService();
if (IS_JODA_TIME_PRESENT) {
conversionService.addConverter(CalendarToDateTimeConverter.INSTANCE);
conversionService.addConverter(CalendarToLocalDateTimeConverter.INSTANCE);
}
this.conversionService = conversionService;
}
/*
@@ -202,38 +244,11 @@ class AuditableBeanWrapperFactory {
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(Calendar value, Field field) {
if (value == null) {
return null;
}
Class<?> targetType = field.getType();
if (Calendar.class.equals(targetType)) {
return value;
}
if (conversionService.canConvert(Calendar.class, targetType)) {
return conversionService.convert(value, targetType);
}
throw new IllegalArgumentException(String.format("Invalid date type for field %s! Supported types are %s.",
field, AnnotationAuditingMetadata.SUPPORTED_DATE_TYPES));
ReflectionUtils.setField(field, target, getDateValueToSet(value, field.getType(), field));
}
}
static enum CalendarToDateTimeConverter implements Converter<Calendar, DateTime> {
private static enum CalendarToDateTimeConverter implements Converter<Calendar, DateTime> {
INSTANCE;
@@ -243,7 +258,7 @@ class AuditableBeanWrapperFactory {
}
}
static enum CalendarToLocalDateTimeConverter implements Converter<Calendar, LocalDateTime> {
private static enum CalendarToLocalDateTimeConverter implements Converter<Calendar, LocalDateTime> {
INSTANCE;

View File

@@ -23,6 +23,9 @@ 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.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.util.Assert;
/**
@@ -35,16 +38,41 @@ public class AuditingHandler implements InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(AuditingHandler.class);
private final AuditableBeanWrapperFactory factory = new AuditableBeanWrapperFactory();
private final AuditableBeanWrapperFactory factory;
private DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE;
private AuditorAware<?> auditorAware;
private boolean dateTimeForNow = true;
private boolean modifyOnCreation = true;
/**
* Creates a new {@link AuditingHandler}.
*
* @deprecated use the constructor taking a {@link MappingContext}.
*/
@Deprecated
public AuditingHandler() {
this.factory = new AuditableBeanWrapperFactory();
}
/**
* Creates a new {@link AuditableBeanWrapper} using the given {@link MappingContext} when looking up auditing metadata
* via reflection.
*
* @param mappingContext must not be {@literal null}.
* @since 1.8
*/
public AuditingHandler(
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext) {
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.factory = new MappingAuditableBeanWrapperFactory(mappingContext);
}
/**
* Setter to inject a {@code AuditorAware} component to retrieve the current auditor.
*
* @param auditorAware the auditorAware to set
* @param auditorAware must not be {@literal null}.
*/
public void setAuditorAware(final AuditorAware<?> auditorAware) {

View File

@@ -15,6 +15,10 @@
*/
package org.springframework.data.auditing;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.MappingContextIsNewStrategyFactory;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.data.support.IsNewStrategyFactory;
import org.springframework.util.Assert;
@@ -31,14 +35,31 @@ public class IsNewAwareAuditingHandler extends AuditingHandler {
private final IsNewStrategyFactory isNewStrategyFactory;
/**
* Creates a new {@link IsNewAwareAuditingHandler} for the given {@link MappingContext}.
*
* @param mappingContext must not be {@literal null}.
* @since 1.8
*/
public IsNewAwareAuditingHandler(
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext) {
super(mappingContext);
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.isNewStrategyFactory = new MappingContextIsNewStrategyFactory(mappingContext);
}
/**
* Creates a new {@link IsNewAwareAuditingHandler} using the given {@link IsNewStrategyFactory}.
*
* @param isNewStrategyFactory must not be {@literal null}.
* @deprecated use constructor taking a {@link MappingContext} directly. Will be removed in 1.9.
*/
@Deprecated
public IsNewAwareAuditingHandler(IsNewStrategyFactory isNewStrategyFactory) {
Assert.notNull(isNewStrategyFactory, "IsNewStrategy must not be null!");
Assert.notNull(isNewStrategyFactory, "IsNewStrategyFactory must not be null!");
this.isNewStrategyFactory = isNewStrategyFactory;
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2014 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.util.Calendar;
import java.util.HashMap;
import java.util.Map;
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.domain.Auditable;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.util.Assert;
/**
* {@link AuditableBeanWrapperFactory} that will create am {@link AuditableBeanWrapper} using mapping information
* obtained from a {@link MappingContext} to detect auditing configuration and eventually invoking setting the auditing
* values.
*
* @author Oliver Gierke
* @since 1.8
*/
class MappingAuditableBeanWrapperFactory extends AuditableBeanWrapperFactory {
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext;
private final Map<Class<?>, MappingAuditingMetadata> metadataCache;
/**
* Creates a new {@link MappingAuditableBeanWrapperFactory} using the given {@link MappingContext}.
*
* @param mappingContext must not be {@literal null}.
*/
public MappingAuditableBeanWrapperFactory(
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext) {
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.mappingContext = mappingContext;
this.metadataCache = new HashMap<Class<?>, MappingAuditingMetadata>();
}
/*
* (non-Javadoc)
* @see org.springframework.data.auditing.AuditableBeanWrapperFactory#getBeanWrapperFor(java.lang.Object)
*/
@Override
public AuditableBeanWrapper getBeanWrapperFor(Object source) {
if (source instanceof Auditable) {
return super.getBeanWrapperFor(source);
}
Class<?> type = source.getClass();
PersistentEntity<?, ?> entity = mappingContext.getPersistentEntity(type);
if (entity == null) {
return super.getBeanWrapperFor(source);
}
MappingAuditingMetadata metadata = metadataCache.get(type);
if (metadata == null) {
metadata = new MappingAuditingMetadata(entity);
metadataCache.put(type, metadata);
}
return metadata.isAuditable() ? new MappingMetadataAuditableBeanWrapper(source, metadata) : null;
}
/**
* Captures {@link PersistentProperty} instances equipped with auditing annotations.
*
* @author Oliver Gierke
* @since 1.8
*/
static class MappingAuditingMetadata {
private final PersistentProperty<?> createdByProperty, createdDateProperty, lastModifiedByProperty,
lastModifiedDateProperty;
/**
* Creates a new {@link MappingAuditingMetadata} instance from the given {@link PersistentEntity}.
*
* @param entity must not be {@literal null}.
*/
public MappingAuditingMetadata(PersistentEntity<?, ? extends PersistentProperty<?>> entity) {
Assert.notNull(entity, "PersistentEntity must not be null!");
this.createdByProperty = entity.getPersistentProperty(CreatedBy.class);
this.createdDateProperty = entity.getPersistentProperty(CreatedDate.class);
this.lastModifiedByProperty = entity.getPersistentProperty(LastModifiedBy.class);
this.lastModifiedDateProperty = entity.getPersistentProperty(LastModifiedDate.class);
}
/**
* Returns whether the {@link PersistentEntity} is auditable at all (read: any of the auditing annotations is
* present).
*
* @return
*/
public boolean isAuditable() {
return createdByProperty != null || createdDateProperty != null || lastModifiedByProperty != null
|| lastModifiedDateProperty != null;
}
}
/**
* {@link AuditableBeanWrapper} using {@link MappingAuditingMetadata} and a {@link BeanWrapper} to set values on
* auditing properties.
*
* @author Oliver Gierke
* @since 1.8
*/
static class MappingMetadataAuditableBeanWrapper extends DateConvertingAuditableBeanWrapper {
private final BeanWrapper<Object> wrapper;
private final MappingAuditingMetadata metadata;
/**
* Creates a new {@link MappingMetadataAuditableBeanWrapper} for the given taregt and
* {@link MappingAuditingMetadata}.
*
* @param target must not be {@literal null}.
* @param metadata must not be {@literal null}.
*/
public MappingMetadataAuditableBeanWrapper(Object target, MappingAuditingMetadata metadata) {
Assert.notNull(target, "Target object must not be null!");
Assert.notNull(metadata, "Auditing metadata must not be null!");
this.wrapper = BeanWrapper.create(target, null);
this.metadata = metadata;
}
/*
* (non-Javadoc)
* @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedBy(java.lang.Object)
*/
@Override
public void setCreatedBy(Object value) {
if (metadata.createdByProperty != null) {
this.wrapper.setProperty(metadata.createdByProperty, value);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedDate(java.util.Calendar)
*/
@Override
public void setCreatedDate(Calendar value) {
PersistentProperty<?> property = metadata.createdDateProperty;
if (property != null) {
this.wrapper.setProperty(property, getDateValueToSet(value, property.getType(), property));
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedBy(java.lang.Object)
*/
@Override
public void setLastModifiedBy(Object value) {
if (metadata.lastModifiedByProperty != null) {
this.wrapper.setProperty(metadata.lastModifiedByProperty, value);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedDate(java.util.Calendar)
*/
@Override
public void setLastModifiedDate(Calendar value) {
PersistentProperty<?> property = metadata.lastModifiedDateProperty;
if (property != null) {
this.wrapper.setProperty(property, getDateValueToSet(value, property.getType(), property));
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
@@ -13,17 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.config;
package org.springframework.data.auditing.config;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.*;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.target.LazyInitTargetSource;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.data.auditing.AuditingHandler;
import org.springframework.data.config.ParsingUtils;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
@@ -37,6 +43,30 @@ public class AuditingHandlerBeanDefinitionParser extends AbstractSingleBeanDefin
private static final String AUDITOR_AWARE_REF = "auditor-aware-ref";
private final String mappingContextBeanName;
private String resolvedBeanName;
/**
* Creates a new {@link AuditingHandlerBeanDefinitionParser} to point to a {@link MappingContext} with the given bean
* name.
*
* @param mappingContextBeanName must not be {@literal null} or empty.
*/
public AuditingHandlerBeanDefinitionParser(String mappingContextBeanName) {
Assert.hasText(mappingContextBeanName, "MappingContext bean name must not be null!");
this.mappingContextBeanName = mappingContextBeanName;
}
/**
* Returns the name of the bean definition the {@link AuditingHandler} was registered under.
*
* @return the resolvedBeanName
*/
public String getResolvedBeanName() {
return resolvedBeanName;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element)
@@ -62,6 +92,8 @@ public class AuditingHandlerBeanDefinitionParser extends AbstractSingleBeanDefin
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
builder.addConstructorArgReference(mappingContextBeanName);
String auditorAwareRef = element.getAttribute(AUDITOR_AWARE_REF);
if (StringUtils.hasText(auditorAwareRef)) {
@@ -73,6 +105,18 @@ public class AuditingHandlerBeanDefinitionParser extends AbstractSingleBeanDefin
ParsingUtils.setPropertyValue(builder, element, "modify-on-creation", "modifyOnCreation");
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#resolveId(org.w3c.dom.Element, org.springframework.beans.factory.support.AbstractBeanDefinition, org.springframework.beans.factory.xml.ParserContext)
*/
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
throws BeanDefinitionStoreException {
this.resolvedBeanName = super.resolveId(element, definition, parserContext);
return resolvedBeanName;
}
private BeanDefinition createLazyInitTargetSourceBeanDefinition(String auditorAwareRef) {
BeanDefinitionBuilder targetSourceBuilder = rootBeanDefinition(LazyInitTargetSource.class);

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2014 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.config;
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.w3c.dom.Element;
/**
* {@link AuditingHandlerBeanDefinitionParser} that will register am {@link IsNewAwareAuditingHandler}. Needs to get the
* bean id of the {@link MappingContext} it shall refer to.
*
* @author Oliver Gierke
* @since 1.5
*/
public class IsNewAwareAuditingHandlerBeanDefinitionParser extends AuditingHandlerBeanDefinitionParser {
/**
* Creates a new {@link IsNewAwareAuditingHandlerBeanDefinitionParser}.
*
* @param mappingContextBeanName must not be {@literal null} or empty.
*/
public IsNewAwareAuditingHandlerBeanDefinitionParser(String mappingContextBeanName) {
super(mappingContextBeanName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.config.AuditingHandlerBeanDefinitionParser#getBeanClass(org.w3c.dom.Element)
*/
@Override
protected Class<?> getBeanClass(Element element) {
return IsNewAwareAuditingHandler.class;
}
}

View File

@@ -1,88 +0,0 @@
/*
* Copyright 2012-2014 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.config;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
import org.springframework.util.Assert;
import org.w3c.dom.Element;
/**
* {@link AuditingHandlerBeanDefinitionParser} that will register am {@link IsNewAwareAuditingHandler}. Needs to get the
* bean id of the
*
* @author Oliver Gierke
*/
public class IsNewAwareAuditingHandlerBeanDefinitionParser extends AuditingHandlerBeanDefinitionParser {
private final String isNewStrategyFactoryBeanId;
private String resolvedBeanName;
/**
* Creates a new {@link IsNewAwareAuditingHandlerBeanDefinitionParser}.
*
* @param isNewStrategyFactoryBeanId must not be {@literal null} or empty.
*/
public IsNewAwareAuditingHandlerBeanDefinitionParser(String isNewStrategyFactoryBeanId) {
Assert.hasText(isNewStrategyFactoryBeanId);
this.isNewStrategyFactoryBeanId = isNewStrategyFactoryBeanId;
}
/**
* Returns the bean name that was used to register the {@link IsNewAwareAuditingHandler}.
*
* @return the resolvedBeanName
*/
public String getResolvedBeanName() {
return resolvedBeanName;
}
/*
* (non-Javadoc)
* @see org.springframework.data.config.AuditingHandlerBeanDefinitionParser#getBeanClass(org.w3c.dom.Element)
*/
@Override
protected Class<?> getBeanClass(Element element) {
return IsNewAwareAuditingHandler.class;
}
/*
* (non-Javadoc)
* @see org.springframework.data.config.AuditingHandlerBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.support.BeanDefinitionBuilder)
*/
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
builder.addConstructorArgReference(isNewStrategyFactoryBeanId);
super.doParse(element, builder);
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#resolveId(org.w3c.dom.Element, org.springframework.beans.factory.support.AbstractBeanDefinition, org.springframework.beans.factory.xml.ParserContext)
*/
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
throws BeanDefinitionStoreException {
this.resolvedBeanName = super.resolveId(element, definition, parserContext);
return resolvedBeanName;
}
}

View File

@@ -96,6 +96,15 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> {
*/
P getPersistentProperty(String name);
/**
* Returns the property equipped with an annotation of the given type.
*
* @param annotationType must not be {@literal null}.
* @return
* @since 1.8
*/
P getPersistentProperty(Class<? extends Annotation> annotationType);
/**
* Returns whether the {@link PersistentEntity} has an id property. If this call returns {@literal true},
* {@link #getIdProperty()} will return a non-{@literal null} value.
@@ -158,6 +167,7 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> {
*
* @param annotationType must not be {@literal null}.
* @return
* @since 1.8
*/
<A extends Annotation> A findAnnotation(Class<A> annotationType);
}

View File

@@ -228,6 +228,33 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
return propertyCache.get(name);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentEntity#getPersistentProperty(java.lang.Class)
*/
@Override
public P getPersistentProperty(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null!");
for (P property : properties) {
if (property.isAnnotationPresent(annotationType)) {
return property;
}
}
for (Association<P> association : associations) {
P property = association.getInverse();
if (property.isAnnotationPresent(annotationType)) {
return property;
}
}
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentEntity#getType()

View File

@@ -46,6 +46,7 @@ public class AuditingHandlerUnitTests {
when(auditorAware.getCurrentAuditor()).thenReturn(user);
}
@SuppressWarnings("deprecation")
protected AuditingHandler getHandler() {
return new AuditingHandler();
}

View File

@@ -25,6 +25,10 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.data.support.IsNewStrategyFactory;
@@ -46,6 +50,7 @@ public class IsNewAwareAuditingHandlerUnitTests extends AuditingHandlerUnitTests
}
@Override
@SuppressWarnings("deprecation")
protected IsNewAwareAuditingHandler getHandler() {
return new IsNewAwareAuditingHandler(factory);
}
@@ -71,4 +76,21 @@ public class IsNewAwareAuditingHandlerUnitTests extends AuditingHandlerUnitTests
assertThat(user.createdDate, is(nullValue()));
assertThat(user.modifiedDate, is(notNullValue()));
}
/**
* @see DATACMNS-365
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullMappingContext() {
new IsNewAwareAuditingHandler(
(MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>>) null);
}
/**
* @see DATACMNS-365
*/
@Test
public void setsUpHandlerWithMappingContext() {
new IsNewAwareAuditingHandler(new SampleMappingContext());
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2014 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 static org.mockito.Mockito.*;
import java.util.GregorianCalendar;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.auditing.AuditableBeanWrapperFactory.AuditableInterfaceBeanWrapper;
import org.springframework.data.domain.Auditable;
import org.springframework.data.mapping.context.SampleMappingContext;
/**
* Unit tests for {@link MappingAuditableBeanWrapperFactory}.
*
* @author Oliver Gierke
* @since 1.8
*/
public class MappingAuditableBeanWrapperFactoryUnitTests {
AuditableBeanWrapperFactory factory;
@Before
public void setUp() {
factory = new MappingAuditableBeanWrapperFactory(new SampleMappingContext());
}
/**
* @see DATACMNS-365
*/
@Test
public void discoversAuditingPropertyOnField() {
Sample sample = new Sample();
AuditableBeanWrapper wrapper = factory.getBeanWrapperFor(sample);
assertThat(wrapper, is(notNullValue()));
wrapper.setCreatedBy("Me!");
assertThat(sample.createdBy, is(notNullValue()));
}
/**
* @see DATACMNS-365
*/
@Test
public void discoversAuditingPropertyOnAccessor() {
Sample sample = new Sample();
AuditableBeanWrapper wrapper = factory.getBeanWrapperFor(sample);
assertThat(wrapper, is(notNullValue()));
wrapper.setLastModifiedBy("Me, too!");
assertThat(sample.lastModifiedBy, is(notNullValue()));
}
/**
* @see DATACMNS-365
*/
@Test
public void settingInavailablePropertyIsNoop() {
Sample sample = new Sample();
AuditableBeanWrapper wrapper = factory.getBeanWrapperFor(sample);
wrapper.setLastModifiedDate(new GregorianCalendar());
}
/**
* @see DATACMNS-365
*/
@Test
public void doesNotReturnWrapperForEntityNotUsingAuditing() {
assertThat(factory.getBeanWrapperFor(new NoAuditing()), is(nullValue()));
}
/**
* @see DATACMNS-365
*/
@Test
public void returnsAuditableWrapperForAuditable() {
assertThat(factory.getBeanWrapperFor(mock(ExtendingAuditable.class)),
is(instanceOf(AuditableInterfaceBeanWrapper.class)));
}
static class Sample {
@CreatedBy private Object createdBy;
private Object lastModifiedBy;
@LastModifiedBy
public Object getLastModifiedBy() {
return lastModifiedBy;
}
}
static class NoAuditing {
}
@SuppressWarnings("serial")
static abstract class ExtendingAuditable implements Auditable<Object, Long> {
}
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2011-2014 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.mapping.model;
import static org.hamcrest.CoreMatchers.*;
@@ -8,16 +23,23 @@ import java.util.Comparator;
import java.util.Iterator;
import java.util.SortedSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentEntitySpec;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.Person;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.mapping.context.SamplePersistentProperty;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.test.util.ReflectionTestUtils;
@@ -29,8 +51,9 @@ import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
public class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
@Mock
T property;
@Rule public ExpectedException exception = ExpectedException.none();
@Mock T property;
@Test
public void assertInvariants() {
@@ -122,13 +145,28 @@ public class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
when(property.isIdProperty()).thenReturn(true);
entity.addPersistentProperty(property);
exception.expect(MappingException.class);
entity.addPersistentProperty(property);
}
try {
entity.addPersistentProperty(property);
fail("Expected MappingException!");
} catch (MappingException e) {
// expected
}
/**
* @see DATACMNS-365
*/
@Test
public void detectsPropertyWithAnnotation() {
SampleMappingContext context = new SampleMappingContext();
PersistentEntity<Object, SamplePersistentProperty> entity = context.getPersistentEntity(Entity.class);
PersistentProperty<?> property = entity.getPersistentProperty(LastModifiedBy.class);
assertThat(property, is(notNullValue()));
assertThat(property.getName(), is("field"));
property = entity.getPersistentProperty(CreatedBy.class);
assertThat(property, is(notNullValue()));
assertThat(property.getName(), is("property"));
assertThat(entity.getPersistentProperty(CreatedDate.class), is(nullValue()));
}
private BasicPersistentEntity<Person, T> createEntity(Comparator<T> comparator) {
@@ -142,5 +180,15 @@ public class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
static class Entity {
@LastModifiedBy String field;
String property;
/**
* @return the property
*/
@CreatedBy
public String getProperty() {
return property;
}
}
}