DATACMNS-1610 - Improved property path access.

We now expose a dedicated PersistentPropertyPathAccessor and optionally take options for both the read and write access to properties. On read, clients can define to eagerly receive null in case one of the path segments is null. By default, we reject those as it indicates that there might be an issue with the backing object if the client assumes it can look up the more deeply nested path to receive a result.

On write access clients can define to either reject, skip or log intermediate null segments where skip means, that the setting is just silently aborted. Clients can also control how to deal with collection and map intermediates. By default we now propagate the attempt to set a value to all collection or map items respectively. This could be potentially extended to provide a filter receiving both the property and property value to selectively propagate those calls in the future.

The separation of these APIs (PersistentPropertyAccessor and PersistentPropertyPathAccessor) is introduced as the latter can be implemented on top of the former and the former potentially being dynamically generated. The logic of the latter can then be clearly separated from the actual individual property lookup. ConvertingPropertyAccessor is now a PersistentPropertyPathAccessor, too, and overrides SimplePersistentPropertyPathAccessor.getTypedProperty(…) to plug in conversion logic. This allows custom collection types being used if the ConversionService backing the accessor can convert from and to Collection or Map respectively.

Deprecated all PersistentPropertyPath-related methods on PersistentPropertyAccessor for removal in 2.3.

MappingAuditableBeanWrapperFactory now uses these new settings to skip both null values as well as collection and map values when setting auditing metadata.

Related tickets: DATACMNS-1438, DATACMNS-1461, DATACMNS-1609.
This commit is contained in:
Oliver Drotbohm
2018-12-17 18:20:33 +01:00
committed by Oliver Drotbohm
parent 70f973f6f9
commit 5cdb3139a4
11 changed files with 933 additions and 40 deletions

View File

@@ -27,10 +27,13 @@ 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.AccessOptions;
import org.springframework.data.mapping.AccessOptions.SetOptions;
import org.springframework.data.mapping.AccessOptions.SetOptions.Propagation;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
@@ -84,7 +87,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap
key -> new MappingAuditingMetadata(context, it.getClass()));
return Optional.<AuditableBeanWrapper<T>> ofNullable(metadata.isAuditable() //
? new MappingMetadataAuditableBeanWrapper<T>(entity.getPropertyAccessor(it), metadata)
? new MappingMetadataAuditableBeanWrapper<T>(entity.getPropertyPathAccessor(it), metadata)
: null);
}).orElseGet(() -> super.getBeanWrapperFor(source));
@@ -160,7 +163,11 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap
*/
static class MappingMetadataAuditableBeanWrapper<T> extends DateConvertingAuditableBeanWrapper<T> {
private final PersistentPropertyAccessor<T> accessor;
private static final SetOptions OPTIONS = AccessOptions.defaultSetOptions() //
.skipNulls() // ;
.withCollectionAndMapPropagation(Propagation.SKIP);
private final PersistentPropertyPathAccessor<T> accessor;
private final MappingAuditingMetadata metadata;
/**
@@ -170,7 +177,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap
* @param accessor must not be {@literal null}.
* @param metadata must not be {@literal null}.
*/
public MappingMetadataAuditableBeanWrapper(PersistentPropertyAccessor<T> accessor,
public MappingMetadataAuditableBeanWrapper(PersistentPropertyPathAccessor<T> accessor,
MappingAuditingMetadata metadata) {
Assert.notNull(accessor, "PersistentPropertyAccessor must not be null!");
@@ -241,20 +248,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap
private <S, P extends PersistentProperty<?>> S setProperty(
PersistentPropertyPaths<?, ? extends PersistentProperty<?>> paths, S value) {
paths.forEach(it -> {
try {
this.accessor.setProperty(it, value);
} catch (MappingException o_O) {
// Ignore null intermediate errors temporarily
if (!o_O.getMessage().contains("on null intermediate")) {
throw o_O;
}
}
});
paths.forEach(it -> this.accessor.setProperty(it, value, OPTIONS));
return value;
}
@@ -262,8 +256,12 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap
private <P extends PersistentProperty<?>> TemporalAccessor setDateProperty(
PersistentPropertyPaths<?, ? extends PersistentProperty<?>> property, TemporalAccessor value) {
property.forEach(it -> this.accessor.setProperty(it,
getDateValueToSet(value, it.getRequiredLeafProperty().getType(), accessor.getBean())));
property.forEach(it -> {
Class<?> type = it.getRequiredLeafProperty().getType();
this.accessor.setProperty(it, getDateValueToSet(value, type, accessor.getBean()), OPTIONS);
});
return value;
}