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:
committed by
Oliver Drotbohm
parent
70f973f6f9
commit
5cdb3139a4
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.With;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.mapping.AccessOptions.SetOptions.SetNulls;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Access options when using {@link PersistentPropertyPathAccessor} to get and set properties. Allows defining how to
|
||||
* handle {@literal null} values, register custom transforming handlers when accessing collections and maps and
|
||||
* propagation settings for how to handle intermediate collection and map values when setting values.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 2.3
|
||||
*/
|
||||
public class AccessOptions {
|
||||
|
||||
/**
|
||||
* Returns the default {@link SetOptions} rejecting setting values when finding an intermediate property value to be
|
||||
* {@literal null}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static SetOptions defaultSetOptions() {
|
||||
return SetOptions.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default {@link GetOptions} rejecting intermediate {@literal null} values when accessing property paths.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static GetOptions defaultGetOptions() {
|
||||
return GetOptions.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access options for getting values for property paths.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public static class GetOptions {
|
||||
|
||||
private static final GetOptions DEFAULT = new GetOptions(new HashMap<>(), GetNulls.REJECT);
|
||||
|
||||
private final Map<PersistentProperty<?>, Function<Object, Object>> handlers;
|
||||
private final @With @Getter GetNulls nullValues;
|
||||
|
||||
/**
|
||||
* How to handle null values during a {@link PersistentPropertyPath} traversal.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public enum GetNulls {
|
||||
|
||||
/**
|
||||
* Reject the path lookup as a {@literal null} value cannot be traversed any further.
|
||||
*/
|
||||
REJECT,
|
||||
|
||||
/**
|
||||
* Returns {@literal null} as the entire path's traversal result.
|
||||
*/
|
||||
EARLY_RETURN;
|
||||
|
||||
public SetOptions.SetNulls toNullHandling() {
|
||||
return REJECT == this ? SetNulls.REJECT : SetNulls.SKIP;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link Function} to post-process values for the given property.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param handler must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public GetOptions registerHandler(PersistentProperty<?> property, Function<Object, Object> handler) {
|
||||
|
||||
Assert.notNull(property, "Property must not be null!");
|
||||
Assert.notNull(handler, "Handler must not be null!");
|
||||
|
||||
Map<PersistentProperty<?>, Function<Object, Object>> newHandlers = new HashMap<>(handlers);
|
||||
newHandlers.put(property, handler);
|
||||
|
||||
return new GetOptions(newHandlers, nullValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link Function} to handle {@link Collection} values for the given property.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param handler must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public GetOptions registerCollectionHandler(PersistentProperty<?> property,
|
||||
Function<? super Collection<?>, Object> handler) {
|
||||
return registerHandler(property, Collection.class, (Function<Object, Object>) handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link Function} to handle {@link List} values for the given property.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param handler must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public GetOptions registerListHandler(PersistentProperty<?> property, Function<? super List<?>, Object> handler) {
|
||||
return registerHandler(property, List.class, (Function<Object, Object>) handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link Function} to handle {@link Set} values for the given property.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param handler must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public GetOptions registerSetHandler(PersistentProperty<?> property, Function<? super Set<?>, Object> handler) {
|
||||
return registerHandler(property, Set.class, (Function<Object, Object>) handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link Function} to handle {@link Map} values for the given property.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param handler must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public GetOptions registerMapHandler(PersistentProperty<?> property, Function<? super Map<?, ?>, Object> handler) {
|
||||
return registerHandler(property, Map.class, (Function<Object, Object>) handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given {@link Function} to post-process values obtained for the given {@link PersistentProperty} for
|
||||
* the given type.
|
||||
*
|
||||
* @param <T> the type of the value to handle.
|
||||
* @param property must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
* @param handler must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public <T> GetOptions registerHandler(PersistentProperty<?> property, Class<T> type,
|
||||
Function<? super T, Object> handler) {
|
||||
|
||||
Assert.isTrue(type.isAssignableFrom(property.getType()), () -> String
|
||||
.format("Cannot register a property handler for %s on a property of type %s!", type, property.getType()));
|
||||
|
||||
Function<Object, T> caster = it -> type.cast(it);
|
||||
|
||||
return registerHandler(property, caster.andThen(handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-processes the value obtained for the given {@link PersistentProperty} using the registered handler.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param value can be {@literal null}.
|
||||
* @return the post-processed value or the value itself if no handlers registered.
|
||||
*/
|
||||
@Nullable
|
||||
Object postProcess(PersistentProperty<?> property, @Nullable Object value) {
|
||||
|
||||
Function<Object, Object> handler = handlers.get(property);
|
||||
|
||||
return handler == null ? value : handler.apply(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access options for setting values for property paths.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@With
|
||||
@AllArgsConstructor
|
||||
public static class SetOptions {
|
||||
|
||||
/**
|
||||
* How to handle intermediate {@literal null} values when setting
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public enum SetNulls {
|
||||
|
||||
/**
|
||||
* Reject {@literal null} values detected when traversing a path to eventually set the leaf property. This will
|
||||
* cause a {@link MappingException} being thrown in that case.
|
||||
*/
|
||||
REJECT,
|
||||
|
||||
/**
|
||||
* Skip setting the value but log an info message to leave a trace why the value wasn't actually set.
|
||||
*/
|
||||
SKIP_AND_LOG,
|
||||
|
||||
/**
|
||||
* Silently skip the attempt to set the value.
|
||||
*/
|
||||
SKIP;
|
||||
}
|
||||
|
||||
/**
|
||||
* How to propagate setting values that cross collection and map properties.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public enum Propagation {
|
||||
|
||||
/**
|
||||
* Skip the setting of values when encountering a collection or map value within the path to traverse.
|
||||
*/
|
||||
SKIP,
|
||||
|
||||
/**
|
||||
* Propagate the setting of values when encountering a collection or map value and set it on all collection or map
|
||||
* members.
|
||||
*/
|
||||
PROPAGATE;
|
||||
}
|
||||
|
||||
private static final SetOptions DEFAULT = new SetOptions();
|
||||
|
||||
private final @Getter SetNulls nullHandling;
|
||||
private final Propagation collectionPropagation, mapPropagation;
|
||||
|
||||
private SetOptions() {
|
||||
|
||||
this.nullHandling = SetNulls.REJECT;
|
||||
this.collectionPropagation = Propagation.PROPAGATE;
|
||||
this.mapPropagation = Propagation.PROPAGATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link AccessOptions} that will cause paths that contain {@literal null} values to be skipped when
|
||||
* setting a property.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public SetOptions skipNulls() {
|
||||
return withNullHandling(SetNulls.SKIP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link AccessOptions} that will cause paths that contain {@literal null} values to be skipped when
|
||||
* setting a property but a log message produced in TRACE level.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public SetOptions skipAndLogNulls() {
|
||||
return withNullHandling(SetNulls.SKIP_AND_LOG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link AccessOptions} that will cause paths that contain {@literal null} values to be skipped when
|
||||
* setting a property.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public SetOptions rejectNulls() {
|
||||
return withNullHandling(SetNulls.REJECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to configure the same {@link Propagation} for both collection and map property path segments.
|
||||
*
|
||||
* @param propagation must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public SetOptions withCollectionAndMapPropagation(Propagation propagation) {
|
||||
|
||||
Assert.notNull(propagation, "Propagation must not be null!");
|
||||
|
||||
return withCollectionPropagation(propagation) //
|
||||
.withMapPropagation(propagation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given property is supposed to be propagated, i.e. if values for it are supposed to be set at
|
||||
* all.
|
||||
*
|
||||
* @param property can be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean propagate(@Nullable PersistentProperty<?> property) {
|
||||
|
||||
if (property == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (property.isCollectionLike() && collectionPropagation.equals(Propagation.SKIP)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (property.isMap() && mapPropagation.equals(Propagation.SKIP)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +288,15 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
|
||||
*/
|
||||
<B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean);
|
||||
|
||||
/**
|
||||
* Returns a {@link PersistentPropertyPathAccessor} to access property values of the given bean.
|
||||
*
|
||||
* @param bean must not be {@literal null}.
|
||||
* @return a new {@link PersistentPropertyPathAccessor}
|
||||
* @since 2.3
|
||||
*/
|
||||
<B> PersistentPropertyPathAccessor<B> getPropertyPathAccessor(B bean);
|
||||
|
||||
/**
|
||||
* Returns the {@link IdentifierAccessor} for the given bean.
|
||||
*
|
||||
|
||||
@@ -392,4 +392,18 @@ public interface PersistentProperty<P extends PersistentProperty<P>> {
|
||||
*/
|
||||
@Nullable
|
||||
Class<?> getAssociationTargetType();
|
||||
|
||||
/**
|
||||
* Returns a {@link PersistentPropertyAccessor} for the current property's owning value.
|
||||
*
|
||||
* @param owner must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 2.3
|
||||
*/
|
||||
default <T> PersistentPropertyAccessor<T> getAccessorForOwner(T owner) {
|
||||
|
||||
Assert.notNull(owner, "Owner must not be null!");
|
||||
|
||||
return getOwner().getPropertyAccessor(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,10 @@ public interface PersistentPropertyAccessor<T> {
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @param value can be {@literal null}.
|
||||
* @since 2.1
|
||||
* @deprecated since 2.3, use {@link PersistentPropertyPathAccessor#setProperty(PersistentPropertyPath, Object)}
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
default void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, @Nullable Object value) {
|
||||
|
||||
Assert.notNull(path, "PersistentPropertyPath must not be null!");
|
||||
@@ -60,6 +63,12 @@ public interface PersistentPropertyAccessor<T> {
|
||||
|
||||
PersistentPropertyPath<? extends PersistentProperty<?>> parentPath = path.getParentPath();
|
||||
PersistentProperty<?> leafProperty = path.getRequiredLeafProperty();
|
||||
PersistentProperty<?> parentProperty = parentPath.isEmpty() ? null : parentPath.getLeafProperty();
|
||||
|
||||
if (parentProperty != null && (parentProperty.isCollectionLike() || parentProperty.isMap())) {
|
||||
throw new MappingException(
|
||||
String.format("Cannot traverse collection or map intermediate %s", parentPath.toDotPath()));
|
||||
}
|
||||
|
||||
Object parent = parentPath.isEmpty() ? getBean() : getProperty(parentPath);
|
||||
|
||||
@@ -67,8 +76,8 @@ public interface PersistentPropertyAccessor<T> {
|
||||
|
||||
String nullIntermediateMessage = "Cannot lookup property %s on null intermediate! Original path was: %s on %s.";
|
||||
|
||||
throw new MappingException(String.format(nullIntermediateMessage, parentPath.getLeafProperty(), path.toDotPath(),
|
||||
getBean().getClass().getName()));
|
||||
throw new MappingException(
|
||||
String.format(nullIntermediateMessage, parentProperty, path.toDotPath(), getBean().getClass().getName()));
|
||||
}
|
||||
|
||||
PersistentPropertyAccessor<?> accessor = parent == getBean() //
|
||||
@@ -104,7 +113,9 @@ public interface PersistentPropertyAccessor<T> {
|
||||
* @param path must not be {@literal null}.
|
||||
* @return
|
||||
* @since 2.1
|
||||
* @deprecated since 2.3, use {@link PersistentPropertyPathAccessor#getProperty(PersistentPropertyPath)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
default Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path) {
|
||||
return getProperty(path, new TraversalContext());
|
||||
@@ -120,8 +131,12 @@ public interface PersistentPropertyAccessor<T> {
|
||||
* @param context must not be {@literal null}.
|
||||
* @return
|
||||
* @since 2.2
|
||||
* @deprecated since 2.3, use
|
||||
* {@link PersistentPropertyPathAccessor#getProperty(PersistentPropertyPath, org.springframework.data.mapping.AccessOptions.GetOptions)}
|
||||
* instead.
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated
|
||||
default Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, TraversalContext context) {
|
||||
|
||||
Object bean = getBean();
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2018-2020 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;
|
||||
|
||||
import org.springframework.data.mapping.AccessOptions.GetOptions;
|
||||
import org.springframework.data.mapping.AccessOptions.SetOptions;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Extension of {@link PersistentPropertyAccessor} that is also able to obtain and set values for
|
||||
* {@link PersistentPropertyPath}s.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 2.3
|
||||
* @soundtrack Sophia Wahnschaffe - Leuchten (Live Session) https://www.youtube.com/watch?v=izZxWiPto5Q
|
||||
*/
|
||||
public interface PersistentPropertyPathAccessor<T> extends PersistentPropertyAccessor<T> {
|
||||
|
||||
/**
|
||||
* Return the value pointed to by the given {@link PersistentPropertyPath}. If the given path is empty, the wrapped
|
||||
* bean is returned.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("deprecation")
|
||||
default Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path) {
|
||||
return PersistentPropertyAccessor.super.getProperty(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value pointed to by the given {@link PersistentPropertyPath}. If the given path is empty, the wrapped
|
||||
* bean is returned. On each path segment value lookup, the resulting value is post-processed by handlers registered
|
||||
* on the given {@link TraversalContext} context. This can be used to unwrap container types that are encountered
|
||||
* during the traversal.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
* @param context must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, GetOptions context);
|
||||
|
||||
/**
|
||||
* Sets the given value for the {@link PersistentProperty} pointed to by the given {@link PersistentPropertyPath}. The
|
||||
* lookup of intermediate values must not yield {@literal null}.
|
||||
*
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @param value can be {@literal null}.
|
||||
* @see AccessOptions#DEFAULT
|
||||
*/
|
||||
void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, @Nullable Object value);
|
||||
|
||||
/**
|
||||
* Sets the given value for the {@link PersistentProperty} pointed to by the given {@link PersistentPropertyPath}
|
||||
* considering the given {@link AccessOptions}.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
* @param value must not be {@literal null}.
|
||||
* @param options must not be {@literal null}.
|
||||
*/
|
||||
void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, @Nullable Object value,
|
||||
SetOptions options);
|
||||
}
|
||||
@@ -455,6 +455,15 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
|
||||
return propertyAccessorFactory.getPropertyAccessor(this, bean);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentEntity#getPropertyPathAccessor(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public <B> PersistentPropertyPathAccessor<B> getPropertyPathAccessor(B bean) {
|
||||
return new SimplePersistentPropertyPathAccessor<>(getPropertyAccessor(bean));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentEntity#getIdentifierAccessor(java.lang.Object)
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.springframework.util.Assert;
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class ConvertingPropertyAccessor<T> implements PersistentPropertyAccessor<T> {
|
||||
public class ConvertingPropertyAccessor<T> extends SimplePersistentPropertyPathAccessor<T> {
|
||||
|
||||
private final PersistentPropertyAccessor<T> accessor;
|
||||
private final ConversionService conversionService;
|
||||
@@ -45,6 +45,8 @@ public class ConvertingPropertyAccessor<T> implements PersistentPropertyAccessor
|
||||
*/
|
||||
public ConvertingPropertyAccessor(PersistentPropertyAccessor<T> accessor, ConversionService conversionService) {
|
||||
|
||||
super(accessor);
|
||||
|
||||
Assert.notNull(accessor, "PersistentPropertyAccessor must not be null!");
|
||||
Assert.notNull(conversionService, "ConversionService must not be null!");
|
||||
|
||||
@@ -70,17 +72,7 @@ public class ConvertingPropertyAccessor<T> implements PersistentPropertyAccessor
|
||||
|
||||
Object converted = convertIfNecessary(value, path.getRequiredLeafProperty().getType());
|
||||
|
||||
PersistentPropertyAccessor.super.setProperty(path, converted);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyAccessor#getProperty(org.springframework.data.mapping.PersistentProperty)
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getProperty(PersistentProperty<?> property) {
|
||||
return accessor.getProperty(property);
|
||||
super.setProperty(path, converted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,13 +91,14 @@ public class ConvertingPropertyAccessor<T> implements PersistentPropertyAccessor
|
||||
return convertIfNecessary(getProperty(property), targetType);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyAccessor#getBean()
|
||||
* @see org.springframework.data.mapping.model.SimplePersistentPropertyPathAccessor#getTypedProperty(org.springframework.data.mapping.PersistentProperty, java.lang.Class)
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public T getBean() {
|
||||
return accessor.getBean();
|
||||
protected <S> S getTypedProperty(PersistentProperty<?> property, Class<S> type) {
|
||||
return convertIfNecessary(super.getTypedProperty(property, type), type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +112,11 @@ public class ConvertingPropertyAccessor<T> implements PersistentPropertyAccessor
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
private <S> S convertIfNecessary(@Nullable Object source, Class<S> type) {
|
||||
return (S) (source == null ? null
|
||||
: type.isAssignableFrom(source.getClass()) ? source : conversionService.convert(source, type));
|
||||
|
||||
return (S) (source == null //
|
||||
? null //
|
||||
: type.isAssignableFrom(source.getClass()) //
|
||||
? source //
|
||||
: conversionService.convert(source, type));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright 2018-2020 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.springframework.data.mapping.AccessOptions.SetOptions.SetNulls.*;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.CollectionFactory;
|
||||
import org.springframework.data.mapping.AccessOptions;
|
||||
import org.springframework.data.mapping.AccessOptions.GetOptions;
|
||||
import org.springframework.data.mapping.AccessOptions.SetOptions;
|
||||
import org.springframework.data.mapping.AccessOptions.SetOptions.SetNulls;
|
||||
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.PersistentPropertyPath;
|
||||
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link PersistentPropertyPathAccessor} that propagates attempts to set property values through collections and map
|
||||
* values. I.e. if a {@link PersistentPropertyPath} contains a path segment pointing to a collection or map based
|
||||
* property, the nested property will be set on all collection elements and map values.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.3
|
||||
* @soundtrack Ron Spielman - Nineth Song (Tip of My Tongue)
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
class SimplePersistentPropertyPathAccessor<T> implements PersistentPropertyPathAccessor<T> {
|
||||
|
||||
private final @NonNull PersistentPropertyAccessor<T> delegate;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyAccessor#getBean()
|
||||
*/
|
||||
@Override
|
||||
public T getBean() {
|
||||
return delegate.getBean();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyAccessor#getProperty(org.springframework.data.mapping.PersistentProperty)
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getProperty(PersistentProperty<?> property) {
|
||||
return delegate.getProperty(property);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyPathAccessor#getProperty(org.springframework.data.mapping.PersistentPropertyPath)
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path) {
|
||||
return getProperty(path, AccessOptions.defaultGetOptions());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyPathAccessor#getProperty(org.springframework.data.mapping.PersistentPropertyPath, org.springframework.data.mapping.PersistentPropertyPathAccessor.Options)
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, GetOptions options) {
|
||||
|
||||
Object bean = getBean();
|
||||
Object current = bean;
|
||||
|
||||
if (path.isEmpty()) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
for (PersistentProperty<?> property : path) {
|
||||
|
||||
if (current == null) {
|
||||
return handleNull(path, options.getNullValues().toNullHandling());
|
||||
}
|
||||
|
||||
PersistentEntity<?, ? extends PersistentProperty<?>> entity = property.getOwner();
|
||||
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(current);
|
||||
|
||||
current = accessor.getProperty(property);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyAccessor#setProperty(org.springframework.data.mapping.PersistentProperty, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void setProperty(PersistentProperty<?> property, @Nullable Object value) {
|
||||
delegate.setProperty(property, value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PersistentPropertyPathAccessor#setProperty(org.springframework.data.mapping.PersistentPropertyPath, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, @Nullable Object value) {
|
||||
setProperty(path, value, AccessOptions.defaultSetOptions());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.model.ConvertingPropertyAccessor#setProperty(org.springframework.data.mapping.PersistentPropertyPath, java.lang.Object)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, @Nullable Object value,
|
||||
SetOptions options) {
|
||||
|
||||
Assert.notNull(path, "PersistentPropertyPath must not be null!");
|
||||
Assert.isTrue(!path.isEmpty(), "PersistentPropertyPath must not be empty!");
|
||||
|
||||
PersistentPropertyPath<? extends PersistentProperty<?>> parentPath = path.getParentPath();
|
||||
PersistentProperty<?> leafProperty = path.getRequiredLeafProperty();
|
||||
|
||||
if (!options.propagate(parentPath.getLeafProperty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object parent = parentPath.isEmpty() ? getBean() : getProperty(parentPath);
|
||||
|
||||
if (parent == null) {
|
||||
handleNull(path, options.getNullHandling());
|
||||
return;
|
||||
}
|
||||
|
||||
if (parent == getBean()) {
|
||||
|
||||
setProperty(leafProperty, value);
|
||||
return;
|
||||
}
|
||||
|
||||
PersistentProperty<?> parentProperty = parentPath.getRequiredLeafProperty();
|
||||
|
||||
Object newValue;
|
||||
|
||||
if (parentProperty.isCollectionLike()) {
|
||||
|
||||
Collection<Object> source = getTypedProperty(parentProperty, Collection.class);
|
||||
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
newValue = source.stream() //
|
||||
.map(it -> setValue(it, leafProperty, value)) //
|
||||
.collect(Collectors.toCollection(() -> CollectionFactory.createApproximateCollection(source, source.size())));
|
||||
|
||||
} else if (Map.class.isInstance(parent)) {
|
||||
|
||||
Map<Object, Object> source = getTypedProperty(parentProperty, Map.class);
|
||||
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Object, Object> result = CollectionFactory.createApproximateMap(source, source.size());
|
||||
|
||||
for (Entry<?, Object> entry : source.entrySet()) {
|
||||
result.put(entry.getKey(), setValue(entry.getValue(), leafProperty, value));
|
||||
}
|
||||
|
||||
newValue = result;
|
||||
|
||||
} else {
|
||||
newValue = setValue(parent, leafProperty, value);
|
||||
}
|
||||
|
||||
if (newValue != parent) {
|
||||
setProperty(parentPath, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path must not be {@literal null}.
|
||||
* @param handling must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private Object handleNull(PersistentPropertyPath<? extends PersistentProperty<?>> path, SetNulls handling) {
|
||||
|
||||
if (SKIP.equals(handling)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String nullIntermediateMessage = "Cannot lookup property %s on null intermediate! Original path was: %s on %s.";
|
||||
|
||||
if (SetNulls.SKIP_AND_LOG.equals(handling)) {
|
||||
LOG.info(nullIntermediateMessage);
|
||||
return null;
|
||||
}
|
||||
|
||||
PersistentPropertyPath<? extends PersistentProperty<?>> parentPath = path.getParentPath();
|
||||
|
||||
throw new MappingException(String.format(nullIntermediateMessage, parentPath.getLeafProperty(), path.toDotPath(),
|
||||
getBean().getClass().getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value for the given {@link PersistentProperty} on the given parent object and returns the potentially
|
||||
* newly created instance.
|
||||
*
|
||||
* @param parent must not be {@literal null}.
|
||||
* @param property must not be {@literal null}.
|
||||
* @param newValue can be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private static Object setValue(Object parent, PersistentProperty<?> property, @Nullable Object newValue) {
|
||||
|
||||
PersistentPropertyAccessor<Object> accessor = property.getAccessorForOwner(parent);
|
||||
accessor.setProperty(property, newValue);
|
||||
return accessor.getBean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the given {@link PersistentProperty} potentially applying type conversion to the given target
|
||||
* type. The default implementation will not attempt any conversion and reject a type mismatch with a
|
||||
* {@link MappingException}.
|
||||
*
|
||||
* @param property will never be {@literal null}.
|
||||
* @param type will never be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
protected <S> S getTypedProperty(PersistentProperty<?> property, Class<S> type) {
|
||||
|
||||
Assert.notNull(property, "Property must not be null!");
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
Object value = getProperty(property);
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!type.isInstance(value)) {
|
||||
throw new MappingException(String.format("Invalid property value type! Need %s but got %s!", //
|
||||
type.getName(), value.getClass().getName()));
|
||||
}
|
||||
|
||||
return type.cast(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user