From 170c25d3555ee954e542e34442853d36122c3077 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 31 Mar 2018 09:01:17 +0200 Subject: [PATCH] DATACMNS-1274 - Auditing now can set metadata in nested objects. Based on the new infrastructure created by DATACMNS-1275, MappingAuditingMetadata now keeps PersistentPropertyPaths to point to the properties reflecting individual auditing metadata items rather than just PersistentPropertyPaths. With that in place we can now find those items in embedded types of the subject entity based on the detection setup of the MappingContext managing metadata for the entity. As that means that multiple paths to a metadata item property can be found, we now use the first path found (the shortest one) for the lookup of the modification date. Related tickets: DATACMNS-1275. --- .../MappingAuditableBeanWrapperFactory.java | 79 +++++++++++-------- ...gAuditableBeanWrapperFactoryUnitTests.java | 44 +++++++++++ 2 files changed, 92 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java b/src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java index 08afabda5..138189971 100644 --- a/src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java +++ b/src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java @@ -15,9 +15,12 @@ */ package org.springframework.data.auditing; +import java.lang.annotation.Annotation; import java.time.temporal.TemporalAccessor; +import java.util.Arrays; import java.util.Map; import java.util.Optional; +import java.util.function.Predicate; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; @@ -27,9 +30,10 @@ import org.springframework.data.domain.Auditable; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.data.util.Optionals; +import org.springframework.data.util.Lazy; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -73,16 +77,14 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap return super.getBeanWrapperFor(source); } - Class type = it.getClass(); + return entities.mapOnContext(it.getClass(), (context, entity) -> { - return entities.getPersistentEntity(type).map(entity -> { + MappingAuditingMetadata metadata = metadataCache.computeIfAbsent(it.getClass(), + key -> new MappingAuditingMetadata(context, it.getClass())); - MappingAuditingMetadata metadata = metadataCache.computeIfAbsent(type, - key -> new MappingAuditingMetadata(entity)); - - return Optional. ofNullable( - metadata.isAuditable() ? new MappingMetadataAuditableBeanWrapper(entity.getPropertyAccessor(it), metadata) - : null); + return Optional. ofNullable(metadata.isAuditable() // + ? new MappingMetadataAuditableBeanWrapper(entity.getPropertyAccessor(it), metadata) + : null); }).orElseGet(() -> super.getBeanWrapperFor(source)); }); @@ -96,22 +98,34 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap */ static class MappingAuditingMetadata { - private final Optional> createdByProperty, createdDateProperty, - lastModifiedByProperty, lastModifiedDateProperty; + private final PersistentPropertyPaths> createdByPaths, createdDatePaths, + lastModifiedByPaths, lastModifiedDatePaths; + + private final Lazy isAuditable; /** * Creates a new {@link MappingAuditingMetadata} instance from the given {@link PersistentEntity}. * * @param entity must not be {@literal null}. */ - public MappingAuditingMetadata(PersistentEntity> entity) { + public

MappingAuditingMetadata(MappingContext> context, Class type) { - Assert.notNull(entity, "PersistentEntity must not be null!"); + Assert.notNull(type, "Type must not be null!"); - this.createdByProperty = Optional.ofNullable(entity.getPersistentProperty(CreatedBy.class)); - this.createdDateProperty = Optional.ofNullable(entity.getPersistentProperty(CreatedDate.class)); - this.lastModifiedByProperty = Optional.ofNullable(entity.getPersistentProperty(LastModifiedBy.class)); - this.lastModifiedDateProperty = Optional.ofNullable(entity.getPersistentProperty(LastModifiedDate.class)); + this.createdByPaths = context.findPersistentPropertyPaths(type, withAnnotation(CreatedBy.class)); + this.createdDatePaths = context.findPersistentPropertyPaths(type, withAnnotation(CreatedDate.class)); + this.lastModifiedByPaths = context.findPersistentPropertyPaths(type, withAnnotation(LastModifiedBy.class)); + this.lastModifiedDatePaths = context.findPersistentPropertyPaths(type, withAnnotation(LastModifiedDate.class)); + + this.isAuditable = Lazy.of( // + () -> Arrays.asList(createdByPaths, createdDatePaths, lastModifiedByPaths, lastModifiedDatePaths) // + .stream() // + .anyMatch(it -> !it.isEmpty())// + ); + } + + private static Predicate> withAnnotation(Class type) { + return t -> t.findAnnotation(type) != null; } /** @@ -121,8 +135,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap * @return */ public boolean isAuditable() { - return Optionals.isAnyPresent(createdByProperty, createdDateProperty, lastModifiedByProperty, - lastModifiedDateProperty); + return isAuditable.get(); } } @@ -161,7 +174,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap @Override public Object setCreatedBy(Object value) { - metadata.createdByProperty.ifPresent(it -> this.accessor.setProperty(it, value)); + metadata.createdByPaths.forEach(it -> this.accessor.setProperty(it, value)); return value; } @@ -172,7 +185,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap */ @Override public TemporalAccessor setCreatedDate(TemporalAccessor value) { - return setDateProperty(metadata.createdDateProperty, value); + return setDateProperty(metadata.createdDatePaths, value); } /* @@ -181,7 +194,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap */ @Override public Object setLastModifiedBy(Object value) { - return setProperty(metadata.lastModifiedByProperty, value); + return setProperty(metadata.lastModifiedByPaths, value); } /* @@ -190,8 +203,11 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap */ @Override public Optional getLastModifiedDate() { - return getAsTemporalAccessor(metadata.lastModifiedDateProperty.map(accessor::getProperty), - TemporalAccessor.class); + + Optional firstValue = metadata.lastModifiedDatePaths.getFirst() // + .map(accessor::getProperty); + + return getAsTemporalAccessor(firstValue, TemporalAccessor.class); } /* @@ -200,21 +216,22 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap */ @Override public TemporalAccessor setLastModifiedDate(TemporalAccessor value) { - return setDateProperty(metadata.lastModifiedDateProperty, value); + return setDateProperty(metadata.lastModifiedDatePaths, value); } - private > T setProperty(Optional

property, T value) { + private > T setProperty( + PersistentPropertyPaths> paths, T value) { - property.ifPresent(it -> this.accessor.setProperty(it, value)); + paths.forEach(it -> this.accessor.setProperty(it, value)); return value; } - private

> TemporalAccessor setDateProperty(Optional

property, - TemporalAccessor value) { + private

> TemporalAccessor setDateProperty( + PersistentPropertyPaths> property, TemporalAccessor value) { - property - .ifPresent(it -> this.accessor.setProperty(it, getDateValueToSet(value, it.getType(), accessor.getBean()))); + property.forEach(it -> this.accessor.setProperty(it, + getDateValueToSet(value, it.getRequiredLeafProperty().getType(), accessor.getBean()))); return value; } diff --git a/src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java b/src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java index 7ab9c7924..38e92862a 100755 --- a/src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactoryUnitTests.java @@ -61,6 +61,7 @@ public class MappingAuditableBeanWrapperFactoryUnitTests { SampleMappingContext context = new SampleMappingContext(); context.getPersistentEntity(Sample.class); context.getPersistentEntity(SampleWithInstant.class); + context.getPersistentEntity(WithEmbedded.class); PersistentEntities entities = new PersistentEntities(Collections.singleton(context)); factory = new MappingAuditableBeanWrapperFactory(entities); @@ -181,6 +182,35 @@ public class MappingAuditableBeanWrapperFactoryUnitTests { assertLastModificationDate(reference, Instant.ofEpochMilli(reference)); } + @Test // DATACMNS-1274 + public void writesNestedAuditingData() { + + WithEmbedded target = new WithEmbedded(); + target.embedded = new Embedded(); + + Optional wrapper = factory.getBeanWrapperFor(target); + + assertThat(wrapper).hasValueSatisfying(it -> { + + Instant now = Instant.now(); + String user = "user"; + + it.setCreatedBy(user); + it.setLastModifiedBy(user); + it.setLastModifiedDate(now); + it.setCreatedDate(now); + + Embedded embedded = target.embedded; + + assertThat(embedded.created).isEqualTo(now); + assertThat(embedded.creator).isEqualTo(user); + assertThat(embedded.modified).isEqualTo(now); + assertThat(embedded.modifier).isEqualTo(user); + + assertThat(it.getLastModifiedDate()).hasValue(now); + }); + } + private void assertLastModificationDate(Object source, TemporalAccessor expected) { Sample sample = new Sample(); @@ -231,4 +261,18 @@ public class MappingAuditableBeanWrapperFactoryUnitTests { static class NoAuditing {} static abstract class ExtendingAuditable implements Auditable {} + + // DATACMNS-1274 + + static class Embedded { + + @CreatedDate Instant created; + @CreatedBy String creator; + @LastModifiedDate Instant modified; + @LastModifiedBy String modifier; + } + + static class WithEmbedded { + Embedded embedded; + } }