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

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

@@ -0,0 +1,130 @@
/*
* 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 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;
/**
* {@link BeanDefinitionParser} that parses an {@link AuditingHandler} {@link BeanDefinition}
*
* @author Oliver Gierke
* @since 1.5
*/
public class AuditingHandlerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
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)
*/
@Override
protected Class<?> getBeanClass(Element element) {
return AuditingHandler.class;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#shouldGenerateId()
*/
@Override
protected boolean shouldGenerateId() {
return true;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.support.BeanDefinitionBuilder)
*/
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
builder.addConstructorArgReference(mappingContextBeanName);
String auditorAwareRef = element.getAttribute(AUDITOR_AWARE_REF);
if (StringUtils.hasText(auditorAwareRef)) {
builder.addPropertyValue("auditorAware", createLazyInitTargetSourceBeanDefinition(auditorAwareRef));
}
ParsingUtils.setPropertyValue(builder, element, "set-dates", "dateTimeForNow");
ParsingUtils.setPropertyReference(builder, element, "date-time-provider-ref", "dateTimeProvider");
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);
targetSourceBuilder.addPropertyValue("targetBeanName", auditorAwareRef);
BeanDefinitionBuilder builder = rootBeanDefinition(ProxyFactoryBean.class);
builder.addPropertyValue("targetSource", targetSourceBuilder.getBeanDefinition());
return builder.getBeanDefinition();
}
}

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