1 /*
2  * Copyright 2008-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 static org.springframework.data.repository.util.ClassUtils.*;
19
20 import java.lang.reflect.Method;
21 import java.util.Set;
22 import java.util.stream.Stream;
23
24 import org.springframework.data.domain.Page;
25 import org.springframework.data.domain.Pageable;
26 import org.springframework.data.domain.Slice;
27 import org.springframework.data.domain.Sort;
28 import org.springframework.data.projection.ProjectionFactory;
29 import org.springframework.data.repository.core.EntityMetadata;
30 import org.springframework.data.repository.core.RepositoryMetadata;
31 import org.springframework.data.repository.util.QueryExecutionConverters;
32 import org.springframework.data.util.ClassTypeInformation;
33 import org.springframework.data.util.Lazy;
34 import org.springframework.data.util.TypeInformation;
35 import org.springframework.util.Assert;
36
37 /**
38  * Abstraction of a method that is designated to execute a finder query. Enriches the standard {@link Method} interface
39  * with specific information that is necessary to construct {@link RepositoryQuery}s for the method.
40  *
41  * @author Oliver Gierke
42  * @author Thomas Darimont
43  * @author Christoph Strobl
44  * @author Maciek Opała
45  * @author Mark Paluch
46  */

47 public class QueryMethod {
48
49     private final RepositoryMetadata metadata;
50     private final Method method;
51     private final Class<?> unwrappedReturnType;
52     private final Parameters<?, ?> parameters;
53     private final ResultProcessor resultProcessor;
54     private final Lazy<Class<?>> domainClass;
55     private final Lazy<Boolean> isCollectionQuery;
56
57     /**
58      * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following
59      * invocations of the method given.
60      *
61      * @param method must not be {@literal null}.
62      * @param metadata must not be {@literal null}.
63      * @param factory must not be {@literal null}.
64      */

65     public QueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) {
66
67         Assert.notNull(method, "Method must not be null!");
68         Assert.notNull(metadata, "Repository metadata must not be null!");
69         Assert.notNull(factory, "ProjectionFactory must not be null!");
70
71         Parameters.TYPES.stream()//
72                 .filter(type -> getNumberOfOccurences(method, type) > 1)//
73                 .findFirst().ifPresent(type -> {
74                     throw new IllegalStateException(
75                             String.format("Method must only one argument of type %s! Offending method: %s", type.getSimpleName(),
76                                     method.toString()));
77                 });
78
79         this.method = method;
80         this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(method);
81         this.parameters = createParameters(method);
82         this.metadata = metadata;
83
84         if (hasParameterOfType(method, Pageable.class)) {
85
86             if (!isStreamQuery()) {
87                 assertReturnTypeAssignable(method, QueryExecutionConverters.getAllowedPageableTypes());
88             }
89
90             if (hasParameterOfType(method, Sort.class)) {
91                 throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. "
92                         + "Use sorting capabilities on Pageable instead! Offending method: %s", method.toString()));
93             }
94         }
95
96         Assert.notNull(this.parameters,
97                 () -> String.format("Parameters extracted from method '%s' must not be null!", method.getName()));
98
99         if (isPageQuery()) {
100             Assert.isTrue(this.parameters.hasPageableParameter(),
101                     String.format("Paging query needs to have a Pageable parameter! Offending method %s", method.toString()));
102         }
103
104         this.domainClass = Lazy.of(() -> {
105
106             Class<?> repositoryDomainClass = metadata.getDomainType();
107             Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);
108
109             return repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass)
110                     ? methodDomainClass
111                     : repositoryDomainClass;
112         });
113
114         this.resultProcessor = new ResultProcessor(this, factory);
115         this.isCollectionQuery = Lazy.of(this::calculateIsCollectionQuery);
116     }
117
118     /**
119      * Creates a {@link Parameters} instance.
120      *
121      * @param method
122      * @return must not return {@literal null}.
123      */

124     protected Parameters<?, ?> createParameters(Method method) {
125         return new DefaultParameters(method);
126     }
127
128     /**
129      * Returns the method's name.
130      *
131      * @return
132      */

133     public String getName() {
134         return method.getName();
135     }
136
137     @SuppressWarnings({ "rawtypes""unchecked" })
138     public EntityMetadata<?> getEntityInformation() {
139         return () -> (Class) getDomainClass();
140     }
141
142     /**
143      * Returns the name of the named query this method belongs to.
144      *
145      * @return
146      */

147     public String getNamedQueryName() {
148         return String.format("%s.%s", getDomainClass().getSimpleName(), method.getName());
149     }
150
151     /**
152      * Returns the domain class the query method is targeted at.
153      *
154      * @return will never be {@literal null}.
155      */

156     protected Class<?> getDomainClass() {
157         return domainClass.get();
158     }
159
160     /**
161      * Returns the type of the object that will be returned.
162      *
163      * @return
164      */

165     public Class<?> getReturnedObjectType() {
166         return metadata.getReturnedDomainClass(method);
167     }
168
169     /**
170      * Returns whether the finder will actually return a collection of entities or a single one.
171      *
172      * @return
173      */

174     public boolean isCollectionQuery() {
175         return isCollectionQuery.get();
176     }
177
178     /**
179      * Returns whether the query method will return a {@link Slice}.
180      *
181      * @return
182      * @since 1.8
183      */

184     public boolean isSliceQuery() {
185         return !isPageQuery() && org.springframework.util.ClassUtils.isAssignable(Slice.class, unwrappedReturnType);
186     }
187
188     /**
189      * Returns whether the finder will return a {@link Page} of results.
190      *
191      * @return
192      */

193     public final boolean isPageQuery() {
194         return org.springframework.util.ClassUtils.isAssignable(Page.class, unwrappedReturnType);
195     }
196
197     /**
198      * Returns whether the query method is a modifying one.
199      *
200      * @return
201      */

202     public boolean isModifyingQuery() {
203         return false;
204     }
205
206     /**
207      * Returns whether the query for this method actually returns entities.
208      *
209      * @return
210      */

211     public boolean isQueryForEntity() {
212         return getDomainClass().isAssignableFrom(getReturnedObjectType());
213     }
214
215     /**
216      * Returns whether the method returns a Stream.
217      *
218      * @return
219      * @since 1.10
220      */

221     public boolean isStreamQuery() {
222         return Stream.class.isAssignableFrom(unwrappedReturnType);
223     }
224
225     /**
226      * Returns the {@link Parameters} wrapper to gain additional information about {@link Method} parameters.
227      *
228      * @return
229      */

230     public Parameters<?, ?> getParameters() {
231         return parameters;
232     }
233
234     /**
235      * Returns the {@link ResultProcessor} to be used with the query method.
236      *
237      * @return the resultFactory
238      */

239     public ResultProcessor getResultProcessor() {
240         return resultProcessor;
241     }
242
243     /*
244      * (non-Javadoc)
245      * @see java.lang.Object#toString()
246      */

247     @Override
248     public String toString() {
249         return method.toString();
250     }
251
252     private boolean calculateIsCollectionQuery() {
253
254         if (isPageQuery() || isSliceQuery()) {
255             return false;
256         }
257
258         Class<?> returnType = method.getReturnType();
259
260         if (QueryExecutionConverters.supports(returnType) && !QueryExecutionConverters.isSingleValue(returnType)) {
261             return true;
262         }
263
264         if (QueryExecutionConverters.supports(unwrappedReturnType)) {
265             return !QueryExecutionConverters.isSingleValue(unwrappedReturnType);
266         }
267
268         return ClassTypeInformation.from(unwrappedReturnType).isCollectionLike();
269     }
270
271     private static Class<? extends Object> potentiallyUnwrapReturnTypeFor(Method method) {
272
273         if (QueryExecutionConverters.supports(method.getReturnType())) {
274
275             // unwrap only one level to handle cases like Future<List<Entity>> correctly.
276
277             TypeInformation<?> componentType = ClassTypeInformation.fromReturnTypeOf(method).getComponentType();
278
279             if (componentType == null) {
280                 throw new IllegalStateException(
281                         String.format("Couldn't find component type for return value of method %s!", method));
282             }
283
284             return componentType.getType();
285         }
286
287         return method.getReturnType();
288     }
289
290     private static void assertReturnTypeAssignable(Method method, Set<Class<?>> types) {
291
292         Assert.notNull(method, "Method must not be null!");
293         Assert.notEmpty(types, "Types must not be null or empty!");
294
295         TypeInformation<?> returnType = ClassTypeInformation.fromReturnTypeOf(method);
296
297         returnType = QueryExecutionConverters.isSingleValue(returnType.getType()) //
298                 ? returnType.getRequiredComponentType() //
299                 : returnType;
300
301         for (Class<?> type : types) {
302             if (type.isAssignableFrom(returnType.getType())) {
303                 return;
304             }
305         }
306
307         throw new IllegalStateException("Method has to have one of the following return types! " + types.toString());
308     }
309 }
310