1 /*
2  * Copyright 2015-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.repository.query;
17
18 import lombok.AccessLevel;
19 import lombok.AllArgsConstructor;
20 import lombok.NonNull;
21 import lombok.RequiredArgsConstructor;
22
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.stream.Stream;
29
30 import javax.annotation.Nonnull;
31
32 import org.springframework.core.CollectionFactory;
33 import org.springframework.core.convert.ConversionService;
34 import org.springframework.core.convert.converter.Converter;
35 import org.springframework.core.convert.support.DefaultConversionService;
36 import org.springframework.data.domain.Slice;
37 import org.springframework.data.projection.ProjectionFactory;
38 import org.springframework.data.repository.util.ReactiveWrapperConverters;
39 import org.springframework.lang.Nullable;
40 import org.springframework.util.Assert;
41
42 /**
43  * A {@link ResultProcessor} to expose metadata about query result element projection and eventually post processing raw
44  * query results into projections and data transfer objects.
45  *
46  * @author Oliver Gierke
47  * @author John Blum
48  * @author Mark Paluch
49  * @author Christoph Strobl
50  * @since 1.12
51  */

52 @AllArgsConstructor(access = AccessLevel.PRIVATE)
53 public class ResultProcessor {
54
55     private final QueryMethod method;
56     private final ProjectingConverter converter;
57     private final ProjectionFactory factory;
58     private final ReturnedType type;
59
60     /**
61      * Creates a new {@link ResultProcessor} from the given {@link QueryMethod} and {@link ProjectionFactory}.
62      *
63      * @param method must not be {@literal null}.
64      * @param factory must not be {@literal null}.
65      */

66     ResultProcessor(QueryMethod method, ProjectionFactory factory) {
67         this(method, factory, method.getReturnedObjectType());
68     }
69
70     /**
71      * Creates a new {@link ResultProcessor} for the given {@link QueryMethod}, {@link ProjectionFactory} and type.
72      *
73      * @param method must not be {@literal null}.
74      * @param factory must not be {@literal null}.
75      * @param type must not be {@literal null}.
76      */

77     private ResultProcessor(QueryMethod method, ProjectionFactory factory, Class<?> type) {
78
79         Assert.notNull(method, "QueryMethod must not be null!");
80         Assert.notNull(factory, "ProjectionFactory must not be null!");
81         Assert.notNull(type, "Type must not be null!");
82
83         this.method = method;
84         this.type = ReturnedType.of(type, method.getDomainClass(), factory);
85         this.converter = new ProjectingConverter(this.type, factory);
86         this.factory = factory;
87     }
88
89     /**
90      * Returns a new {@link ResultProcessor} with a new projection type obtained from the given {@link ParameterAccessor}.
91      *
92      * @param accessor must not be {@literal null}.
93      * @return
94      */

95     public ResultProcessor withDynamicProjection(ParameterAccessor accessor) {
96
97         Assert.notNull(accessor, "Parameter accessor must not be null!");
98
99         Class<?> projection = accessor.findDynamicProjection();
100
101         return projection == null //
102                 ? this //
103                 : withType(projection);
104     }
105
106     /**
107      * Returns the {@link ReturnedType}.
108      *
109      * @return
110      */

111     public ReturnedType getReturnedType() {
112         return type;
113     }
114
115     /**
116      * Post-processes the given query result.
117      *
118      * @param source can be {@literal null}.
119      * @return
120      */

121     @Nullable
122     public <T> T processResult(@Nullable Object source) {
123         return processResult(source, NoOpConverter.INSTANCE);
124     }
125
126     /**
127      * Post-processes the given query result using the given preparing {@link Converter} to potentially prepare collection
128      * elements.
129      *
130      * @param source can be {@literal null}.
131      * @param preparingConverter must not be {@literal null}.
132      * @return
133      */

134     @Nullable
135     @SuppressWarnings("unchecked")
136     public <T> T processResult(@Nullable Object source, Converter<Object, Object> preparingConverter) {
137
138         if (source == null || type.isInstance(source) || !type.isProjecting()) {
139             return (T) source;
140         }
141
142         Assert.notNull(preparingConverter, "Preparing converter must not be null!");
143
144         ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter).and(this.converter);
145
146         if (source instanceof Slice && method.isPageQuery() || method.isSliceQuery()) {
147             return (T) ((Slice<?>) source).map(converter::convert);
148         }
149
150         if (source instanceof Collection && method.isCollectionQuery()) {
151
152             Collection<?> collection = (Collection<?>) source;
153             Collection<Object> target = createCollectionFor(collection);
154
155             for (Object columns : collection) {
156                 target.add(type.isInstance(columns) ? columns : converter.convert(columns));
157             }
158
159             return (T) target;
160         }
161
162         if (source instanceof Stream && method.isStreamQuery()) {
163             return (T) ((Stream<Object>) source).map(t -> type.isInstance(t) ? t : converter.convert(t));
164         }
165
166         if (ReactiveWrapperConverters.supports(source.getClass())) {
167             return (T) ReactiveWrapperConverters.map(source, converter::convert);
168         }
169
170         return (T) converter.convert(source);
171     }
172
173     private ResultProcessor withType(Class<?> type) {
174
175         ReturnedType returnedType = ReturnedType.of(type, method.getDomainClass(), factory);
176         return new ResultProcessor(method, converter.withType(returnedType), factory, returnedType);
177     }
178
179     /**
180      * Creates a new {@link Collection} for the given source. Will try to create an instance of the source collection's
181      * type first falling back to creating an approximate collection if the former fails.
182      *
183      * @param source must not be {@literal null}.
184      * @return
185      */

186     private static Collection<Object> createCollectionFor(Collection<?> source) {
187
188         try {
189             return CollectionFactory.createCollection(source.getClass(), source.size());
190         } catch (RuntimeException o_O) {
191             return CollectionFactory.createApproximateCollection(source, source.size());
192         }
193     }
194
195     @RequiredArgsConstructor(staticName = "of")
196     private static class ChainingConverter implements Converter<Object, Object> {
197
198         private final @NonNull Class<?> targetType;
199         private final @NonNull Converter<Object, Object> delegate;
200
201         /**
202          * Returns a new {@link ChainingConverter} that hands the elements resulting from the current conversion to the
203          * given {@link Converter}.
204          *
205          * @param converter must not be {@literal null}.
206          * @return
207          */

208         public ChainingConverter and(final Converter<Object, Object> converter) {
209
210             Assert.notNull(converter, "Converter must not be null!");
211
212             return new ChainingConverter(targetType, source -> {
213
214                 Object intermediate = ChainingConverter.this.convert(source);
215
216                 return intermediate == null || targetType.isInstance(intermediate) ? intermediate
217                         : converter.convert(intermediate);
218             });
219         }
220
221         /*
222          * (non-Javadoc)
223          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
224          */

225         @Nullable
226         @Override
227         public Object convert(Object source) {
228             return delegate.convert(source);
229         }
230     }
231
232     /**
233      * A simple {@link Converter} that will return the source value as is.
234      *
235      * @author Oliver Gierke
236      * @since 1.12
237      */

238     private static enum NoOpConverter implements Converter<Object, Object> {
239
240         INSTANCE;
241
242         /*
243          * (non-Javadoc)
244          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
245          */

246         @Nonnull
247         @Override
248         public Object convert(Object source) {
249             return source;
250         }
251     }
252
253     @RequiredArgsConstructor
254     private static class ProjectingConverter implements Converter<Object, Object> {
255
256         private final @NonNull ReturnedType type;
257         private final @NonNull ProjectionFactory factory;
258         private final @NonNull ConversionService conversionService;
259
260         /**
261          * Creates a new {@link ProjectingConverter} for the given {@link ReturnedType} and {@link ProjectionFactory}.
262          *
263          * @param type must not be {@literal null}.
264          * @param factory must not be {@literal null}.
265          */

266         ProjectingConverter(ReturnedType type, ProjectionFactory factory) {
267             this(type, factory, DefaultConversionService.getSharedInstance());
268         }
269
270         /**
271          * Creates a new {@link ProjectingConverter} for the given {@link ReturnedType}.
272          *
273          * @param type must not be {@literal null}.
274          * @return
275          */

276         ProjectingConverter withType(ReturnedType type) {
277
278             Assert.notNull(type, "ReturnedType must not be null!");
279
280             return new ProjectingConverter(type, factory, conversionService);
281         }
282
283         /*
284          * (non-Javadoc)
285          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
286          */

287         @Nullable
288         @Override
289         public Object convert(Object source) {
290
291             Class<?> targetType = type.getReturnedType();
292
293             if (targetType.isInterface()) {
294                 return factory.createProjection(targetType, getProjectionTarget(source));
295             }
296
297             return conversionService.convert(source, targetType);
298         }
299
300         private Object getProjectionTarget(Object source) {
301
302             if (source != null && source.getClass().isArray()) {
303                 source = Arrays.asList((Object[]) source);
304             }
305
306             if (source instanceof Collection) {
307                 return toMap((Collection<?>) source, type.getInputProperties());
308             }
309
310             return source;
311         }
312
313         private static Map<String, Object> toMap(Collection<?> values, List<String> names) {
314
315             int i = 0;
316             Map<String, Object> result = new HashMap<>(values.size());
317
318             for (Object element : values) {
319                 result.put(names.get(i++), element);
320             }
321
322             return result;
323         }
324     }
325 }
326