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