diff --git a/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java b/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java index 9ed9f4d17..5258db6b7 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java +++ b/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java @@ -107,6 +107,22 @@ public interface PersistentPropertyAccessor { */ @Nullable default Object getProperty(PersistentPropertyPath> path) { + return getProperty(path, new TraversalContext()); + } + + /** + * 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 + * @since 2.2 + */ + @Nullable + default Object getProperty(PersistentPropertyPath> path, TraversalContext context) { Object bean = getBean(); Object current = bean; @@ -128,7 +144,7 @@ public interface PersistentPropertyAccessor { PersistentEntity> entity = property.getOwner(); PersistentPropertyAccessor accessor = entity.getPropertyAccessor(current); - current = accessor.getProperty(property); + current = context.postProcess(property, accessor.getProperty(property)); } return current; diff --git a/src/main/java/org/springframework/data/mapping/TraversalContext.java b/src/main/java/org/springframework/data/mapping/TraversalContext.java new file mode 100644 index 000000000..5a5caa777 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/TraversalContext.java @@ -0,0 +1,146 @@ +/* + * Copyright 2019 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 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.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A context object for lookups of values for {@link PersistentPropertyPaths} via a {@link PersistentPropertyAccessor}. + * It allows to register functions to post-process the objects returned for a particular property, so that the + * subsequent traversal would rather use the processed object. This is especially helpful if you need to traverse paths + * that contain {@link Collection}s and {@link Map} that usually need indices and keys to reasonably traverse nested + * properties. + * + * @author Oliver Drotbohm + * @since 2.2 + * @soundtrack 8-Bit Misfits - Crash Into Me (Dave Matthews Band cover) + */ +public class TraversalContext { + + private Map, Function> handlers = new HashMap<>(); + + /** + * 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 TraversalContext registerHandler(PersistentProperty property, Function handler) { + + Assert.notNull(property, "Property must not be null!"); + Assert.notNull(handler, "Handler must not be null!"); + + handlers.put(property, handler); + + return this; + } + + /** + * 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 TraversalContext registerCollectionHandler(PersistentProperty property, + Function, Object> handler) { + return registerHandler(property, Collection.class, (Function) 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 TraversalContext registerListHandler(PersistentProperty property, + Function, Object> handler) { + return registerHandler(property, List.class, (Function) 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 TraversalContext registerSetHandler(PersistentProperty property, Function, Object> handler) { + return registerHandler(property, Set.class, (Function) 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 TraversalContext registerMapHandler(PersistentProperty property, + Function, Object> handler) { + return registerHandler(property, Map.class, (Function) handler); + } + + /** + * Registers the given {@link Function} to post-process values obtained for the given {@link PersistentProperty} for + * the given type. + * + * @param 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 TraversalContext registerHandler(PersistentProperty property, Class type, + Function 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 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 handler = handlers.get(property); + + return handler == null ? value : handler.apply(value); + } +} diff --git a/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java b/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java index d86f0d3fc..91aead75a 100644 --- a/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java @@ -19,10 +19,20 @@ import static org.assertj.core.api.Assertions.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.Value; import lombok.experimental.Wither; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; + import org.junit.Test; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.mapping.context.SampleMappingContext; @@ -133,6 +143,53 @@ public class PersistentPropertyAccessorUnitTests { assertThat(convertingAccessor.getBean().getCustomer().getFirstname()).isEqualTo("2"); } + @Test // DATACMNS-1555 + public void usesTraversalContextToTraverseCollections() { + + WithContext withContext = WithContext.builder() // + .collection(Collections.singleton("value")) // + .list(Collections.singletonList("value")) // + .set(Collections.singleton("value")) // + .map(Collections.singletonMap("key", "value")) // + .string(" value ") // + .build(); + + Spec collectionHelper = Spec.of("collection", + (context, property) -> context.registerCollectionHandler(property, it -> it.iterator().next())); + Spec listHelper = Spec.of("list", (context, property) -> context.registerListHandler(property, it -> it.get(0))); + Spec setHelper = Spec.of("set", + (context, property) -> context.registerSetHandler(property, it -> it.iterator().next())); + Spec mapHelper = Spec.of("map", (context, property) -> context.registerMapHandler(property, it -> it.get("key"))); + Spec stringHelper = Spec.of("string", + (context, property) -> context.registerHandler(property, String.class, it -> it.trim())); + + Stream.of(collectionHelper, listHelper, setHelper, mapHelper, stringHelper).forEach(it -> { + + PersistentEntity entity = context.getPersistentEntity(WithContext.class); + PersistentProperty property = entity.getRequiredPersistentProperty(it.name); + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(withContext); + + TraversalContext traversalContext = it.registrar.apply(new TraversalContext(), property); + + PersistentPropertyPath propertyPath = context.getPersistentPropertyPath(it.name, + WithContext.class); + + assertThat(accessor.getProperty(propertyPath, traversalContext)).isEqualTo("value"); + }); + } + + @Test // DATACMNS-1555 + public void traversalContextRejectsInvalidPropertyHandler() { + + PersistentEntity entity = context.getPersistentEntity(WithContext.class); + PersistentProperty property = entity.getRequiredPersistentProperty("collection"); + + TraversalContext traversal = new TraversalContext(); + + assertThatExceptionOfType(IllegalArgumentException.class) // + .isThrownBy(() -> traversal.registerHandler(property, Map.class, Function.identity())); + } + @Value static class Order { Customer customer; @@ -157,4 +214,23 @@ public class PersistentPropertyAccessorUnitTests { static class Outer { NestedImmutable immutable; } + + // DATACMNS-1555 + + @Builder + static class WithContext { + + Collection collection; + List list; + Set set; + Map map; + String string; + } + + @Value(staticConstructor = "of") + static class Spec { + + String name; + BiFunction, TraversalContext> registrar; + } }