From e9bcca11be30caec582093612e2a1ea321e2a5db Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 18 Mar 2014 15:52:19 +0100 Subject: [PATCH] DATACMNS-365 - Enhanced auditing subsystem to work with accessor annotations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- Spring Data Commons.sonargraph | 38 +++- .../auditing/AuditableBeanWrapperFactory.java | 105 +++++---- .../data/auditing/AuditingHandler.java | 32 ++- .../auditing/IsNewAwareAuditingHandler.java | 23 +- .../MappingAuditableBeanWrapperFactory.java | 206 ++++++++++++++++++ .../AuditingHandlerBeanDefinitionParser.java | 48 +++- ...reAuditingHandlerBeanDefinitionParser.java | 48 ++++ ...reAuditingHandlerBeanDefinitionParser.java | 88 -------- .../data/mapping/PersistentEntity.java | 10 + .../mapping/model/BasicPersistentEntity.java | 27 +++ .../auditing/AuditingHandlerUnitTests.java | 1 + .../IsNewAwareAuditingHandlerUnitTests.java | 22 ++ ...gAuditableBeanWrapperFactoryUnitTests.java | 126 +++++++++++ .../model/BasicPersistentEntityUnitTests.java | 64 +++++- 14 files changed, 680 insertions(+), 158 deletions(-) create mode 100644 src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java rename src/main/java/org/springframework/data/{ => auditing}/config/AuditingHandlerBeanDefinitionParser.java (64%) create mode 100644 src/main/java/org/springframework/data/auditing/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java delete mode 100644 src/main/java/org/springframework/data/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java create mode 100644 src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java diff --git a/Spring Data Commons.sonargraph b/Spring Data Commons.sonargraph index f21de9ad7..ae6bbda82 100644 --- a/Spring Data Commons.sonargraph +++ b/Spring Data Commons.sonargraph @@ -99,6 +99,20 @@ + + + + + + + + + + + + + + @@ -135,12 +149,6 @@ - - - - - - @@ -151,18 +159,24 @@ - - - - - - + + + + + + + + + + + + diff --git a/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java b/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java index 84e17353d..54b1bb506 100644 --- a/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java +++ b/src/main/java/org/springframework/data/auditing/AuditableBeanWrapperFactory.java @@ -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 { + private static enum CalendarToDateTimeConverter implements Converter { INSTANCE; @@ -243,7 +258,7 @@ class AuditableBeanWrapperFactory { } } - static enum CalendarToLocalDateTimeConverter implements Converter { + private static enum CalendarToLocalDateTimeConverter implements Converter { INSTANCE; diff --git a/src/main/java/org/springframework/data/auditing/AuditingHandler.java b/src/main/java/org/springframework/data/auditing/AuditingHandler.java index 10f29858c..7912359e7 100644 --- a/src/main/java/org/springframework/data/auditing/AuditingHandler.java +++ b/src/main/java/org/springframework/data/auditing/AuditingHandler.java @@ -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 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) { diff --git a/src/main/java/org/springframework/data/auditing/IsNewAwareAuditingHandler.java b/src/main/java/org/springframework/data/auditing/IsNewAwareAuditingHandler.java index fc8837fbf..84c397962 100644 --- a/src/main/java/org/springframework/data/auditing/IsNewAwareAuditingHandler.java +++ b/src/main/java/org/springframework/data/auditing/IsNewAwareAuditingHandler.java @@ -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 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; } diff --git a/src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java b/src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java new file mode 100644 index 000000000..4ae837e54 --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java @@ -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 PersistentProperty> mappingContext; + private final Map, MappingAuditingMetadata> metadataCache; + + /** + * Creates a new {@link MappingAuditableBeanWrapperFactory} using the given {@link MappingContext}. + * + * @param mappingContext must not be {@literal null}. + */ + public MappingAuditableBeanWrapperFactory( + MappingContext, ? extends PersistentProperty> mappingContext) { + + Assert.notNull(mappingContext, "MappingContext must not be null!"); + + this.mappingContext = mappingContext; + this.metadataCache = new HashMap, 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> 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 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)); + } + } + } +} diff --git a/src/main/java/org/springframework/data/config/AuditingHandlerBeanDefinitionParser.java b/src/main/java/org/springframework/data/auditing/config/AuditingHandlerBeanDefinitionParser.java similarity index 64% rename from src/main/java/org/springframework/data/config/AuditingHandlerBeanDefinitionParser.java rename to src/main/java/org/springframework/data/auditing/config/AuditingHandlerBeanDefinitionParser.java index 99f879925..1348d148d 100644 --- a/src/main/java/org/springframework/data/config/AuditingHandlerBeanDefinitionParser.java +++ b/src/main/java/org/springframework/data/auditing/config/AuditingHandlerBeanDefinitionParser.java @@ -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); diff --git a/src/main/java/org/springframework/data/auditing/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java b/src/main/java/org/springframework/data/auditing/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java new file mode 100644 index 000000000..fc7c55a21 --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java @@ -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; + } +} diff --git a/src/main/java/org/springframework/data/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java b/src/main/java/org/springframework/data/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java deleted file mode 100644 index 15fcd594b..000000000 --- a/src/main/java/org/springframework/data/config/IsNewAwareAuditingHandlerBeanDefinitionParser.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/org/springframework/data/mapping/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java index bb033b218..543fe39b7 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/PersistentEntity.java @@ -96,6 +96,15 @@ public interface PersistentEntity> { */ 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 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> { * * @param annotationType must not be {@literal null}. * @return + * @since 1.8 */ A findAnnotation(Class annotationType); } diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index 73c25d209..e0e4a4244 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -228,6 +228,33 @@ public class BasicPersistentEntity> implement return propertyCache.get(name); } + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getPersistentProperty(java.lang.Class) + */ + @Override + public P getPersistentProperty(Class annotationType) { + + Assert.notNull(annotationType, "Annotation type must not be null!"); + + for (P property : properties) { + if (property.isAnnotationPresent(annotationType)) { + return property; + } + } + + for (Association

association : associations) { + + P property = association.getInverse(); + + if (property.isAnnotationPresent(annotationType)) { + return property; + } + } + + return null; + } + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#getType() diff --git a/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java b/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java index b32f80663..c42ee0c08 100644 --- a/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java +++ b/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java @@ -46,6 +46,7 @@ public class AuditingHandlerUnitTests { when(auditorAware.getCurrentAuditor()).thenReturn(user); } + @SuppressWarnings("deprecation") protected AuditingHandler getHandler() { return new AuditingHandler(); } diff --git a/src/test/java/org/springframework/data/auditing/IsNewAwareAuditingHandlerUnitTests.java b/src/test/java/org/springframework/data/auditing/IsNewAwareAuditingHandlerUnitTests.java index eed7c6b49..a15a8da93 100644 --- a/src/test/java/org/springframework/data/auditing/IsNewAwareAuditingHandlerUnitTests.java +++ b/src/test/java/org/springframework/data/auditing/IsNewAwareAuditingHandlerUnitTests.java @@ -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 PersistentProperty>) null); + } + + /** + * @see DATACMNS-365 + */ + @Test + public void setsUpHandlerWithMappingContext() { + new IsNewAwareAuditingHandler(new SampleMappingContext()); + } } diff --git a/src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java b/src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java new file mode 100644 index 000000000..34ef05606 --- /dev/null +++ b/src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java @@ -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 { + + } +} diff --git a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java index 6f39ca517..ff93584c6 100644 --- a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java @@ -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> { - @Mock - T property; + @Rule public ExpectedException exception = ExpectedException.none(); + + @Mock T property; @Test public void assertInvariants() { @@ -122,13 +145,28 @@ public class BasicPersistentEntityUnitTests> { 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 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 createEntity(Comparator comparator) { @@ -142,5 +180,15 @@ public class BasicPersistentEntityUnitTests> { static class Entity { + @LastModifiedBy String field; + String property; + + /** + * @return the property + */ + @CreatedBy + public String getProperty() { + return property; + } } }