From e0f2d65d73a3606e18492715279c8e7a32bfc277 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 19 Jul 2019 07:56:42 +0200 Subject: [PATCH] DATACMNS-1555 - Support for extracting properties during a PersistentPropertyPath traversal. PersistentPropertyAccessor now has an overloaded method to pass in a TraversalContext that can be prepared to property handlers for PersistentProperties. During the traversal, the handler is called with the value for the property so that it can e.g. extract list elements, map values etc. --- .../mapping/PersistentPropertyAccessor.java | 18 ++- .../data/mapping/TraversalContext.java | 146 ++++++++++++++++++ .../PersistentPropertyAccessorUnitTests.java | 76 +++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/data/mapping/TraversalContext.java 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; + } }