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;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.domain.Auditable;
import org.springframework.data.mapping.MappingException;
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.Lazy;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrapperFactory {
private final PersistentEntities entities;
private final Map<Class<?>, MappingAuditingMetadata> metadataCache;
public MappingAuditableBeanWrapperFactory(PersistentEntities entities) {
Assert.notNull(entities, "PersistentEntities must not be null!");
this.entities = entities;
this.metadataCache = new ConcurrentReferenceHashMap<>();
}
@Override
public <T> Optional<AuditableBeanWrapper<T>> getBeanWrapperFor(T source) {
return Optional.of(source).flatMap(it -> {
if (it instanceof Auditable) {
return super.getBeanWrapperFor(source);
}
return entities.mapOnContext(it.getClass(), (context, entity) -> {
MappingAuditingMetadata metadata = metadataCache.computeIfAbsent(it.getClass(),
key -> new MappingAuditingMetadata(context, it.getClass()));
return Optional.<AuditableBeanWrapper<T>> ofNullable(metadata.isAuditable()
? new MappingMetadataAuditableBeanWrapper<T>(entity.getPropertyAccessor(it), metadata)
: null);
}).orElseGet(() -> super.getBeanWrapperFor(source));
});
}
static class MappingAuditingMetadata {
private static final Predicate<? super PersistentProperty<?>> HAS_COLLECTION_PROPERTY = it -> it.isCollectionLike()
|| it.isMap();
private final PersistentPropertyPaths<?, ? extends PersistentProperty<?>> createdByPaths, createdDatePaths,
lastModifiedByPaths, lastModifiedDatePaths;
private final Lazy<Boolean> isAuditable;
public <P> MappingAuditingMetadata(MappingContext<?, ? extends PersistentProperty<?>> context, Class<?> type) {
Assert.notNull(type, "Type must not be null!");
this.createdByPaths = findPropertyPaths(type, CreatedBy.class, context);
this.createdDatePaths = findPropertyPaths(type, CreatedDate.class, context);
this.lastModifiedByPaths = findPropertyPaths(type, LastModifiedBy.class, context);
this.lastModifiedDatePaths = findPropertyPaths(type, LastModifiedDate.class, context);
this.isAuditable = Lazy.of(
() -> Arrays.asList(createdByPaths, createdDatePaths, lastModifiedByPaths, lastModifiedDatePaths)
.stream()
.anyMatch(it -> !it.isEmpty())
);
}
public boolean isAuditable() {
return isAuditable.get();
}
private PersistentPropertyPaths<?, ? extends PersistentProperty<?>> findPropertyPaths(Class<?> type,
Class<? extends Annotation> annotation, MappingContext<?, ? extends PersistentProperty<?>> context) {
return context
.findPersistentPropertyPaths(type, withAnnotation(annotation))
.dropPathIfSegmentMatches(HAS_COLLECTION_PROPERTY);
}
private static Predicate<PersistentProperty<?>> withAnnotation(Class<? extends Annotation> type) {
return t -> t.findAnnotation(type) != null;
}
}
static class MappingMetadataAuditableBeanWrapper<T> extends DateConvertingAuditableBeanWrapper<T> {
private final PersistentPropertyAccessor<T> accessor;
private final MappingAuditingMetadata metadata;
public MappingMetadataAuditableBeanWrapper(PersistentPropertyAccessor<T> accessor,
MappingAuditingMetadata metadata) {
Assert.notNull(accessor, "PersistentPropertyAccessor must not be null!");
Assert.notNull(metadata, "Auditing metadata must not be null!");
this.accessor = accessor;
this.metadata = metadata;
}
@Override
public Object setCreatedBy(Object value) {
return setProperty(metadata.createdByPaths, value);
}
@Override
public TemporalAccessor setCreatedDate(TemporalAccessor value) {
return setDateProperty(metadata.createdDatePaths, value);
}
@Override
public Object setLastModifiedBy(Object value) {
return setProperty(metadata.lastModifiedByPaths, value);
}
@Override
public Optional<TemporalAccessor> getLastModifiedDate() {
Optional<Object> firstValue = metadata.lastModifiedDatePaths.getFirst()
.map(accessor::getProperty);
return getAsTemporalAccessor(firstValue, TemporalAccessor.class);
}
@Override
public TemporalAccessor setLastModifiedDate(TemporalAccessor value) {
return setDateProperty(metadata.lastModifiedDatePaths, value);
}
@Override
public T getBean() {
return accessor.getBean();
}
private <S> S setProperty(PersistentPropertyPaths<?, ? extends PersistentProperty<?>> paths, S value) {
paths.forEach(it -> {
try {
this.accessor.setProperty(it, value);
} catch (MappingException o_O) {
potentiallyRethrowException(o_O);
}
});
return value;
}
private TemporalAccessor setDateProperty(PersistentPropertyPaths<?, ? extends PersistentProperty<?>> property,
TemporalAccessor value) {
property.forEach(it -> {
try {
this.accessor.setProperty(it,
getDateValueToSet(value, it.getRequiredLeafProperty().getType(), accessor.getBean()));
} catch (MappingException o_O) {
potentiallyRethrowException(o_O);
}
});
return value;
}
private static void potentiallyRethrowException(MappingException o_O) {
if (!o_O.getMessage().contains("on null intermediate")) {
throw o_O;
}
}
}
}