1 /*
2 * Copyright 2020 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 lombok.AllArgsConstructor;
19 import lombok.Getter;
20 import lombok.With;
21
22 import java.util.Collection;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.function.Function;
28
29 import org.springframework.data.mapping.AccessOptions.SetOptions.SetNulls;
30 import org.springframework.lang.Nullable;
31 import org.springframework.util.Assert;
32
33 /**
34 * Access options when using {@link PersistentPropertyPathAccessor} to get and set properties. Allows defining how to
35 * handle {@literal null} values, register custom transforming handlers when accessing collections and maps and
36 * propagation settings for how to handle intermediate collection and map values when setting values.
37 *
38 * @author Oliver Drotbohm
39 * @since 2.3
40 */
41 public class AccessOptions {
42
43 /**
44 * Returns the default {@link SetOptions} rejecting setting values when finding an intermediate property value to be
45 * {@literal null}.
46 *
47 * @return
48 */
49 public static SetOptions defaultSetOptions() {
50 return SetOptions.DEFAULT;
51 }
52
53 /**
54 * Returns the default {@link GetOptions} rejecting intermediate {@literal null} values when accessing property paths.
55 *
56 * @return
57 */
58 public static GetOptions defaultGetOptions() {
59 return GetOptions.DEFAULT;
60 }
61
62 /**
63 * Access options for getting values for property paths.
64 *
65 * @author Oliver Drotbohm
66 */
67 @AllArgsConstructor
68 public static class GetOptions {
69
70 private static final GetOptions DEFAULT = new GetOptions(new HashMap<>(), GetNulls.REJECT);
71
72 private final Map<PersistentProperty<?>, Function<Object, Object>> handlers;
73 private final @With @Getter GetNulls nullValues;
74
75 /**
76 * How to handle null values during a {@link PersistentPropertyPath} traversal.
77 *
78 * @author Oliver Drotbohm
79 */
80 public enum GetNulls {
81
82 /**
83 * Reject the path lookup as a {@literal null} value cannot be traversed any further.
84 */
85 REJECT,
86
87 /**
88 * Returns {@literal null} as the entire path's traversal result.
89 */
90 EARLY_RETURN;
91
92 public SetOptions.SetNulls toNullHandling() {
93 return REJECT == this ? SetNulls.REJECT : SetNulls.SKIP;
94 }
95 }
96
97 /**
98 * Registers a {@link Function} to post-process values for the given property.
99 *
100 * @param property must not be {@literal null}.
101 * @param handler must not be {@literal null}.
102 * @return
103 */
104 public GetOptions registerHandler(PersistentProperty<?> property, Function<Object, Object> handler) {
105
106 Assert.notNull(property, "Property must not be null!");
107 Assert.notNull(handler, "Handler must not be null!");
108
109 Map<PersistentProperty<?>, Function<Object, Object>> newHandlers = new HashMap<>(handlers);
110 newHandlers.put(property, handler);
111
112 return new GetOptions(newHandlers, nullValues);
113 }
114
115 /**
116 * Registers a {@link Function} to handle {@link Collection} values for the given property.
117 *
118 * @param property must not be {@literal null}.
119 * @param handler must not be {@literal null}.
120 * @return
121 */
122 @SuppressWarnings("unchecked")
123 public GetOptions registerCollectionHandler(PersistentProperty<?> property,
124 Function<? super Collection<?>, Object> handler) {
125 return registerHandler(property, Collection.class, (Function<Object, Object>) handler);
126 }
127
128 /**
129 * Registers a {@link Function} to handle {@link List} values for the given property.
130 *
131 * @param property must not be {@literal null}.
132 * @param handler must not be {@literal null}.
133 * @return
134 */
135 @SuppressWarnings("unchecked")
136 public GetOptions registerListHandler(PersistentProperty<?> property, Function<? super List<?>, Object> handler) {
137 return registerHandler(property, List.class, (Function<Object, Object>) handler);
138 }
139
140 /**
141 * Registers a {@link Function} to handle {@link Set} values for the given property.
142 *
143 * @param property must not be {@literal null}.
144 * @param handler must not be {@literal null}.
145 * @return
146 */
147 @SuppressWarnings("unchecked")
148 public GetOptions registerSetHandler(PersistentProperty<?> property, Function<? super Set<?>, Object> handler) {
149 return registerHandler(property, Set.class, (Function<Object, Object>) handler);
150 }
151
152 /**
153 * Registers a {@link Function} to handle {@link Map} values for the given property.
154 *
155 * @param property must not be {@literal null}.
156 * @param handler must not be {@literal null}.
157 * @return
158 */
159 @SuppressWarnings("unchecked")
160 public GetOptions registerMapHandler(PersistentProperty<?> property, Function<? super Map<?, ?>, Object> handler) {
161 return registerHandler(property, Map.class, (Function<Object, Object>) handler);
162 }
163
164 /**
165 * Registers the given {@link Function} to post-process values obtained for the given {@link PersistentProperty} for
166 * the given type.
167 *
168 * @param <T> the type of the value to handle.
169 * @param property must not be {@literal null}.
170 * @param type must not be {@literal null}.
171 * @param handler must not be {@literal null}.
172 * @return
173 */
174 public <T> GetOptions registerHandler(PersistentProperty<?> property, Class<T> type,
175 Function<? super T, Object> handler) {
176
177 Assert.isTrue(type.isAssignableFrom(property.getType()), () -> String
178 .format("Cannot register a property handler for %s on a property of type %s!", type, property.getType()));
179
180 Function<Object, T> caster = it -> type.cast(it);
181
182 return registerHandler(property, caster.andThen(handler));
183 }
184
185 /**
186 * Post-processes the value obtained for the given {@link PersistentProperty} using the registered handler.
187 *
188 * @param property must not be {@literal null}.
189 * @param value can be {@literal null}.
190 * @return the post-processed value or the value itself if no handlers registered.
191 */
192 @Nullable
193 Object postProcess(PersistentProperty<?> property, @Nullable Object value) {
194
195 Function<Object, Object> handler = handlers.get(property);
196
197 return handler == null ? value : handler.apply(value);
198 }
199 }
200
201 /**
202 * Access options for setting values for property paths.
203 *
204 * @author Oliver Drotbohm
205 */
206 @With
207 @AllArgsConstructor
208 public static class SetOptions {
209
210 /**
211 * How to handle intermediate {@literal null} values when setting
212 *
213 * @author Oliver Drotbohm
214 */
215 public enum SetNulls {
216
217 /**
218 * Reject {@literal null} values detected when traversing a path to eventually set the leaf property. This will
219 * cause a {@link MappingException} being thrown in that case.
220 */
221 REJECT,
222
223 /**
224 * Skip setting the value but log an info message to leave a trace why the value wasn't actually set.
225 */
226 SKIP_AND_LOG,
227
228 /**
229 * Silently skip the attempt to set the value.
230 */
231 SKIP;
232 }
233
234 /**
235 * How to propagate setting values that cross collection and map properties.
236 *
237 * @author Oliver Drotbohm
238 */
239 public enum Propagation {
240
241 /**
242 * Skip the setting of values when encountering a collection or map value within the path to traverse.
243 */
244 SKIP,
245
246 /**
247 * Propagate the setting of values when encountering a collection or map value and set it on all collection or map
248 * members.
249 */
250 PROPAGATE;
251 }
252
253 private static final SetOptions DEFAULT = new SetOptions();
254
255 private final @Getter SetNulls nullHandling;
256 private final Propagation collectionPropagation, mapPropagation;
257
258 private SetOptions() {
259
260 this.nullHandling = SetNulls.REJECT;
261 this.collectionPropagation = Propagation.PROPAGATE;
262 this.mapPropagation = Propagation.PROPAGATE;
263 }
264
265 /**
266 * Returns a new {@link AccessOptions} that will cause paths that contain {@literal null} values to be skipped when
267 * setting a property.
268 *
269 * @return
270 */
271 public SetOptions skipNulls() {
272 return withNullHandling(SetNulls.SKIP);
273 }
274
275 /**
276 * Returns a new {@link AccessOptions} that will cause paths that contain {@literal null} values to be skipped when
277 * setting a property but a log message produced in TRACE level.
278 *
279 * @return
280 */
281 public SetOptions skipAndLogNulls() {
282 return withNullHandling(SetNulls.SKIP_AND_LOG);
283 }
284
285 /**
286 * Returns a new {@link AccessOptions} that will cause paths that contain {@literal null} values to be skipped when
287 * setting a property.
288 *
289 * @return
290 */
291 public SetOptions rejectNulls() {
292 return withNullHandling(SetNulls.REJECT);
293 }
294
295 /**
296 * Shortcut to configure the same {@link Propagation} for both collection and map property path segments.
297 *
298 * @param propagation must not be {@literal null}.
299 * @return
300 */
301 public SetOptions withCollectionAndMapPropagation(Propagation propagation) {
302
303 Assert.notNull(propagation, "Propagation must not be null!");
304
305 return withCollectionPropagation(propagation) //
306 .withMapPropagation(propagation);
307 }
308
309 /**
310 * Returns whether the given property is supposed to be propagated, i.e. if values for it are supposed to be set at
311 * all.
312 *
313 * @param property can be {@literal null}.
314 * @return
315 */
316 public boolean propagate(@Nullable PersistentProperty<?> property) {
317
318 if (property == null) {
319 return true;
320 }
321
322 if (property.isCollectionLike() && collectionPropagation.equals(Propagation.SKIP)) {
323 return false;
324 }
325
326 if (property.isMap() && mapPropagation.equals(Propagation.SKIP)) {
327 return false;
328 }
329
330 return true;
331 }
332 }
333 }
334