Skip to content

Commit e0f2d65

Browse files
committed
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.
1 parent a0bcc5e commit e0f2d65

File tree

3 files changed

+239
-1
lines changed

3 files changed

+239
-1
lines changed

src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ default void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>>
107107
*/
108108
@Nullable
109109
default Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path) {
110+
return getProperty(path, new TraversalContext());
111+
}
112+
113+
/**
114+
* Return the value pointed to by the given {@link PersistentPropertyPath}. If the given path is empty, the wrapped
115+
* bean is returned. On each path segment value lookup, the resulting value is post-processed by handlers registered
116+
* on the given {@link TraversalContext} context. This can be used to unwrap container types that are encountered
117+
* during the traversal.
118+
*
119+
* @param path must not be {@literal null}.
120+
* @param context must not be {@literal null}.
121+
* @return
122+
* @since 2.2
123+
*/
124+
@Nullable
125+
default Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, TraversalContext context) {
110126

111127
Object bean = getBean();
112128
Object current = bean;
@@ -128,7 +144,7 @@ default Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?
128144
PersistentEntity<?, ? extends PersistentProperty<?>> entity = property.getOwner();
129145
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(current);
130146

131-
current = accessor.getProperty(property);
147+
current = context.postProcess(property, accessor.getProperty(property));
132148
}
133149

134150
return current;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mapping;
17+
18+
import java.util.Collection;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Set;
23+
import java.util.function.Function;
24+
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* A context object for lookups of values for {@link PersistentPropertyPaths} via a {@link PersistentPropertyAccessor}.
30+
* It allows to register functions to post-process the objects returned for a particular property, so that the
31+
* subsequent traversal would rather use the processed object. This is especially helpful if you need to traverse paths
32+
* that contain {@link Collection}s and {@link Map} that usually need indices and keys to reasonably traverse nested
33+
* properties.
34+
*
35+
* @author Oliver Drotbohm
36+
* @since 2.2
37+
* @soundtrack 8-Bit Misfits - Crash Into Me (Dave Matthews Band cover)
38+
*/
39+
public class TraversalContext {
40+
41+
private Map<PersistentProperty<?>, Function<Object, Object>> handlers = new HashMap<>();
42+
43+
/**
44+
* Registers a {@link Function} to post-process values for the given property.
45+
*
46+
* @param property must not be {@literal null}.
47+
* @param handler must not be {@literal null}.
48+
* @return
49+
*/
50+
public TraversalContext registerHandler(PersistentProperty<?> property, Function<Object, Object> handler) {
51+
52+
Assert.notNull(property, "Property must not be null!");
53+
Assert.notNull(handler, "Handler must not be null!");
54+
55+
handlers.put(property, handler);
56+
57+
return this;
58+
}
59+
60+
/**
61+
* Registers a {@link Function} to handle {@link Collection} values for the given property.
62+
*
63+
* @param property must not be {@literal null}.
64+
* @param handler must not be {@literal null}.
65+
* @return
66+
*/
67+
@SuppressWarnings("unchecked")
68+
public TraversalContext registerCollectionHandler(PersistentProperty<?> property,
69+
Function<? super Collection<?>, Object> handler) {
70+
return registerHandler(property, Collection.class, (Function<Object, Object>) handler);
71+
}
72+
73+
/**
74+
* Registers a {@link Function} to handle {@link List} values for the given property.
75+
*
76+
* @param property must not be {@literal null}.
77+
* @param handler must not be {@literal null}.
78+
* @return
79+
*/
80+
@SuppressWarnings("unchecked")
81+
public TraversalContext registerListHandler(PersistentProperty<?> property,
82+
Function<? super List<?>, Object> handler) {
83+
return registerHandler(property, List.class, (Function<Object, Object>) handler);
84+
}
85+
86+
/**
87+
* Registers a {@link Function} to handle {@link Set} values for the given property.
88+
*
89+
* @param property must not be {@literal null}.
90+
* @param handler must not be {@literal null}.
91+
* @return
92+
*/
93+
@SuppressWarnings("unchecked")
94+
public TraversalContext registerSetHandler(PersistentProperty<?> property, Function<? super Set<?>, Object> handler) {
95+
return registerHandler(property, Set.class, (Function<Object, Object>) handler);
96+
}
97+
98+
/**
99+
* Registers a {@link Function} to handle {@link Map} values for the given property.
100+
*
101+
* @param property must not be {@literal null}.
102+
* @param handler must not be {@literal null}.
103+
* @return
104+
*/
105+
@SuppressWarnings("unchecked")
106+
public TraversalContext registerMapHandler(PersistentProperty<?> property,
107+
Function<? super Map<?, ?>, Object> handler) {
108+
return registerHandler(property, Map.class, (Function<Object, Object>) handler);
109+
}
110+
111+
/**
112+
* Registers the given {@link Function} to post-process values obtained for the given {@link PersistentProperty} for
113+
* the given type.
114+
*
115+
* @param <T> the type of the value to handle.
116+
* @param property must not be {@literal null}.
117+
* @param type must not be {@literal null}.
118+
* @param handler must not be {@literal null}.
119+
* @return
120+
*/
121+
public <T> TraversalContext registerHandler(PersistentProperty<?> property, Class<T> type,
122+
Function<? super T, Object> handler) {
123+
124+
Assert.isTrue(type.isAssignableFrom(property.getType()), () -> String
125+
.format("Cannot register a property handler for %s on a property of type %s!", type, property.getType()));
126+
127+
Function<Object, T> caster = it -> type.cast(it);
128+
129+
return registerHandler(property, caster.andThen(handler));
130+
}
131+
132+
/**
133+
* Post-processes the value obtained for the given {@link PersistentProperty} using the registered handler.
134+
*
135+
* @param property must not be {@literal null}.
136+
* @param value can be {@literal null}.
137+
* @return the post-processed value or the value itself if no handlers registered.
138+
*/
139+
@Nullable
140+
Object postProcess(PersistentProperty<?> property, @Nullable Object value) {
141+
142+
Function<Object, Object> handler = handlers.get(property);
143+
144+
return handler == null ? value : handler.apply(value);
145+
}
146+
}

src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,20 @@
1919

2020
import lombok.AccessLevel;
2121
import lombok.AllArgsConstructor;
22+
import lombok.Builder;
2223
import lombok.Data;
2324
import lombok.Value;
2425
import lombok.experimental.Wither;
2526

27+
import java.util.Collection;
28+
import java.util.Collections;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.Set;
32+
import java.util.function.BiFunction;
33+
import java.util.function.Function;
34+
import java.util.stream.Stream;
35+
2636
import org.junit.Test;
2737
import org.springframework.core.convert.support.DefaultConversionService;
2838
import org.springframework.data.mapping.context.SampleMappingContext;
@@ -133,6 +143,53 @@ public void shouldConvertToPropertyPathLeafType() {
133143
assertThat(convertingAccessor.getBean().getCustomer().getFirstname()).isEqualTo("2");
134144
}
135145

146+
@Test // DATACMNS-1555
147+
public void usesTraversalContextToTraverseCollections() {
148+
149+
WithContext withContext = WithContext.builder() //
150+
.collection(Collections.singleton("value")) //
151+
.list(Collections.singletonList("value")) //
152+
.set(Collections.singleton("value")) //
153+
.map(Collections.singletonMap("key", "value")) //
154+
.string(" value ") //
155+
.build();
156+
157+
Spec collectionHelper = Spec.of("collection",
158+
(context, property) -> context.registerCollectionHandler(property, it -> it.iterator().next()));
159+
Spec listHelper = Spec.of("list", (context, property) -> context.registerListHandler(property, it -> it.get(0)));
160+
Spec setHelper = Spec.of("set",
161+
(context, property) -> context.registerSetHandler(property, it -> it.iterator().next()));
162+
Spec mapHelper = Spec.of("map", (context, property) -> context.registerMapHandler(property, it -> it.get("key")));
163+
Spec stringHelper = Spec.of("string",
164+
(context, property) -> context.registerHandler(property, String.class, it -> it.trim()));
165+
166+
Stream.of(collectionHelper, listHelper, setHelper, mapHelper, stringHelper).forEach(it -> {
167+
168+
PersistentEntity<Object, SamplePersistentProperty> entity = context.getPersistentEntity(WithContext.class);
169+
PersistentProperty<?> property = entity.getRequiredPersistentProperty(it.name);
170+
PersistentPropertyAccessor<WithContext> accessor = entity.getPropertyAccessor(withContext);
171+
172+
TraversalContext traversalContext = it.registrar.apply(new TraversalContext(), property);
173+
174+
PersistentPropertyPath<SamplePersistentProperty> propertyPath = context.getPersistentPropertyPath(it.name,
175+
WithContext.class);
176+
177+
assertThat(accessor.getProperty(propertyPath, traversalContext)).isEqualTo("value");
178+
});
179+
}
180+
181+
@Test // DATACMNS-1555
182+
public void traversalContextRejectsInvalidPropertyHandler() {
183+
184+
PersistentEntity<Object, SamplePersistentProperty> entity = context.getPersistentEntity(WithContext.class);
185+
PersistentProperty<?> property = entity.getRequiredPersistentProperty("collection");
186+
187+
TraversalContext traversal = new TraversalContext();
188+
189+
assertThatExceptionOfType(IllegalArgumentException.class) //
190+
.isThrownBy(() -> traversal.registerHandler(property, Map.class, Function.identity()));
191+
}
192+
136193
@Value
137194
static class Order {
138195
Customer customer;
@@ -157,4 +214,23 @@ static class NestedImmutable {
157214
static class Outer {
158215
NestedImmutable immutable;
159216
}
217+
218+
// DATACMNS-1555
219+
220+
@Builder
221+
static class WithContext {
222+
223+
Collection<String> collection;
224+
List<String> list;
225+
Set<String> set;
226+
Map<String, String> map;
227+
String string;
228+
}
229+
230+
@Value(staticConstructor = "of")
231+
static class Spec {
232+
233+
String name;
234+
BiFunction<TraversalContext, PersistentProperty<?>, TraversalContext> registrar;
235+
}
160236
}

0 commit comments

Comments
 (0)