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.jpa.repository.query;
17
18 import java.lang.annotation.Annotation;
19 import java.lang.reflect.Method;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Optional;
25 import java.util.Set;
26
27 import javax.persistence.LockModeType;
28 import javax.persistence.QueryHint;
29
30 import org.springframework.core.annotation.AnnotatedElementUtils;
31 import org.springframework.core.annotation.AnnotationUtils;
32 import org.springframework.data.jpa.provider.QueryExtractor;
33 import org.springframework.data.jpa.repository.EntityGraph;
34 import org.springframework.data.jpa.repository.Lock;
35 import org.springframework.data.jpa.repository.Modifying;
36 import org.springframework.data.jpa.repository.Query;
37 import org.springframework.data.jpa.repository.QueryHints;
38 import org.springframework.data.projection.ProjectionFactory;
39 import org.springframework.data.repository.core.RepositoryMetadata;
40 import org.springframework.data.repository.query.Parameter;
41 import org.springframework.data.repository.query.Parameters;
42 import org.springframework.data.repository.query.QueryMethod;
43 import org.springframework.data.util.Lazy;
44 import org.springframework.lang.Nullable;
45 import org.springframework.util.Assert;
46 import org.springframework.util.StringUtils;
47
48 /**
49  * JPA specific extension of {@link QueryMethod}.
50  *
51  * @author Oliver Gierke
52  * @author Thomas Darimont
53  * @author Christoph Strobl
54  * @author Nicolas Cirigliano
55  * @author Mark Paluch
56  * @author Сергей Цыпанов
57  * @author Réda Housni Alaoui
58  */

59 public class JpaQueryMethod extends QueryMethod {
60
61     /**
62      * @see <a href=
63      *      "https://download.oracle.com/otn-pub/jcp/persistence-2.0-fr-eval-oth-JSpec/persistence-2_0-final-spec.pdf">JPA
64      *      2.0 Specification 2.2 Persistent Fields and Properties Page 23 - Top paragraph.</a>
65      */

66     private static final Set<Class<?>> NATIVE_ARRAY_TYPES;
67     private static final StoredProcedureAttributeSource storedProcedureAttributeSource = StoredProcedureAttributeSource.INSTANCE;
68
69     static {
70
71         Set<Class<?>> types = new HashSet<>();
72         types.add(byte[].class);
73         types.add(Byte[].class);
74         types.add(char[].class);
75         types.add(Character[].class);
76
77         NATIVE_ARRAY_TYPES = Collections.unmodifiableSet(types);
78     }
79
80     private final QueryExtractor extractor;
81     private final Method method;
82
83     private @Nullable StoredProcedureAttributes storedProcedureAttributes;
84     private final Lazy<LockModeType> lockModeType;
85     private final Lazy<QueryHints> queryHints;
86     private final Lazy<JpaEntityGraph> jpaEntityGraph;
87     private final Lazy<Modifying> modifying;
88     private final Lazy<Boolean> isNativeQuery;
89     private final Lazy<Boolean> isCollectionQuery;
90     private final Lazy<Boolean> isProcedureQuery;
91     private final Lazy<JpaEntityMetadata<?>> entityMetadata;
92
93     /**
94      * Creates a {@link JpaQueryMethod}.
95      *
96      * @param method must not be {@literal null}
97      * @param metadata must not be {@literal null}
98      * @param factory must not be {@literal null}
99      * @param extractor must not be {@literal null}
100      */

101     protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
102             QueryExtractor extractor) {
103
104         super(method, metadata, factory);
105
106         Assert.notNull(method, "Method must not be null!");
107         Assert.notNull(extractor, "Query extractor must not be null!");
108
109         this.method = method;
110         this.extractor = extractor;
111         this.lockModeType = Lazy
112                 .of(() -> (LockModeType) Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Lock.class)) //
113                         .map(AnnotationUtils::getValue) //
114                         .orElse(null));
115
116         this.queryHints = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, QueryHints.class));
117         this.modifying = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, Modifying.class));
118         this.jpaEntityGraph = Lazy.of(() -> {
119
120             EntityGraph entityGraph = AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class);
121
122             if (entityGraph == null) {
123                 return null;
124             }
125
126             return new JpaEntityGraph(entityGraph, getNamedQueryName());
127         });
128         this.isNativeQuery = Lazy.of(() -> getAnnotationValue("nativeQuery", Boolean.class));
129         this.isCollectionQuery = Lazy
130                 .of(() -> super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(method.getReturnType()));
131         this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null);
132         this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass()));
133
134         Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
135                 String.format("Modifying method must not contain %s!", Parameters.TYPES));
136         assertParameterNamesInAnnotatedQuery();
137     }
138
139     private void assertParameterNamesInAnnotatedQuery() {
140
141         String annotatedQuery = getAnnotatedQuery();
142
143         if (!DeclaredQuery.of(annotatedQuery).hasNamedParameter()) {
144             return;
145         }
146
147         for (Parameter parameter : getParameters()) {
148
149             if (!parameter.isNamedParameter()) {
150                 continue;
151             }
152
153             if (StringUtils.isEmpty(annotatedQuery)
154                     || !annotatedQuery.contains(String.format(":%s", parameter.getName().get()))
155                             && !annotatedQuery.contains(String.format("#%s", parameter.getName().get()))) {
156                 throw new IllegalStateException(
157                         String.format("Using named parameters for method %s but parameter '%s' not found in annotated query '%s'!",
158                                 method, parameter.getName(), annotatedQuery));
159             }
160         }
161     }
162
163     /*
164      * (non-Javadoc)
165      * @see org.springframework.data.repository.query.QueryMethod#getEntityInformation()
166      */

167     @Override
168     @SuppressWarnings({ "rawtypes""unchecked" })
169     public JpaEntityMetadata<?> getEntityInformation() {
170         return this.entityMetadata.get();
171     }
172
173     /**
174      * Returns whether the finder is a modifying one.
175      *
176      * @return
177      */

178     @Override
179     public boolean isModifyingQuery() {
180         return modifying.getNullable() != null;
181     }
182
183     /**
184      * Returns all {@link QueryHint}s annotated at this class. Note, that {@link QueryHints}
185      *
186      * @return
187      */

188     List<QueryHint> getHints() {
189
190         QueryHints hints = this.queryHints.getNullable();
191         if (hints != null) {
192             return Arrays.asList(hints.value());
193         }
194
195         return Collections.emptyList();
196     }
197
198     /**
199      * Returns the {@link LockModeType} to be used for the query.
200      *
201      * @return
202      */

203     @Nullable
204     LockModeType getLockModeType() {
205         return lockModeType.getNullable();
206     }
207
208     /**
209      * Returns the {@link EntityGraph} to be used for the query.
210      *
211      * @return
212      * @since 1.6
213      */

214     @Nullable
215     JpaEntityGraph getEntityGraph() {
216         return jpaEntityGraph.getNullable();
217     }
218
219     /**
220      * Returns whether the potentially configured {@link QueryHint}s shall be applied when triggering the count query for
221      * pagination.
222      *
223      * @return
224      */

225     boolean applyHintsToCountQuery() {
226
227         QueryHints hints = this.queryHints.getNullable();
228         return hints != null ? hints.forCounting() : false;
229     }
230
231     /**
232      * Returns the {@link QueryExtractor}.
233      *
234      * @return
235      */

236     QueryExtractor getQueryExtractor() {
237         return extractor;
238     }
239
240     /**
241      * Returns the actual return type of the method.
242      *
243      * @return
244      */

245     Class<?> getReturnType() {
246         return method.getReturnType();
247     }
248
249     /**
250      * Returns the query string declared in a {@link Query} annotation or {@literal nullif neither the annotation found
251      * nor the attribute was specified.
252      *
253      * @return
254      */

255     @Nullable
256     String getAnnotatedQuery() {
257
258         String query = getAnnotationValue("value", String.class);
259         return StringUtils.hasText(query) ? query : null;
260     }
261
262     /**
263      * Returns the required query string declared in a {@link Query} annotation or throws {@link IllegalStateException} if
264      * neither the annotation found nor the attribute was specified.
265      *
266      * @return
267      * @throws IllegalStateException if no {@link Query} annotation is present or the query is empty.
268      * @since 2.0
269      */

270     String getRequiredAnnotatedQuery() throws IllegalStateException {
271
272         String query = getAnnotatedQuery();
273
274         if (query != null) {
275             return query;
276         }
277
278         throw new IllegalStateException(String.format("No annotated query found for query method %s!", getName()));
279     }
280
281     /**
282      * Returns the countQuery string declared in a {@link Query} annotation or {@literal nullif neither the annotation
283      * found nor the attribute was specified.
284      *
285      * @return
286      */

287     @Nullable
288     String getCountQuery() {
289
290         String countQuery = getAnnotationValue("countQuery", String.class);
291         return StringUtils.hasText(countQuery) ? countQuery : null;
292     }
293
294     /**
295      * Returns the count query projection string declared in a {@link Query} annotation or {@literal nullif neither the
296      * annotation found nor the attribute was specified.
297      *
298      * @return
299      * @since 1.6
300      */

301     @Nullable
302     String getCountQueryProjection() {
303
304         String countProjection = getAnnotationValue("countProjection", String.class);
305         return StringUtils.hasText(countProjection) ? countProjection : null;
306     }
307
308     /**
309      * Returns whether the backing query is a native one.
310      *
311      * @return
312      */

313     boolean isNativeQuery() {
314         return this.isNativeQuery.get();
315     }
316
317     /*
318      * (non-Javadoc)
319      * @see org.springframework.data.repository.query.QueryMethod#getNamedQueryName()
320      */

321     @Override
322     public String getNamedQueryName() {
323
324         String annotatedName = getAnnotationValue("name", String.class);
325         return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName();
326     }
327
328     /**
329      * Returns the name of the {@link NamedQuery} that shall be used for count queries.
330      *
331      * @return
332      */

333     String getNamedCountQueryName() {
334
335         String annotatedName = getAnnotationValue("countName", String.class);
336         return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count";
337     }
338
339     /**
340      * Returns whether we should flush automatically for modifying queries.
341      *
342      * @return whether we should flush automatically.
343      */

344     boolean getFlushAutomatically() {
345         return getMergedOrDefaultAnnotationValue("flushAutomatically", Modifying.class, Boolean.class);
346     }
347
348     /**
349      * Returns whether we should clear automatically for modifying queries.
350      *
351      * @return whether we should clear automatically.
352      */

353     boolean getClearAutomatically() {
354         return getMergedOrDefaultAnnotationValue("clearAutomatically", Modifying.class, Boolean.class);
355     }
356
357     /**
358      * Returns the {@link Query} annotation's attribute casted to the given type or default value if no annotation
359      * available.
360      *
361      * @param attribute
362      * @param type
363      * @return
364      */

365     private <T> T getAnnotationValue(String attribute, Class<T> type) {
366         return getMergedOrDefaultAnnotationValue(attribute, Query.class, type);
367     }
368
369     @SuppressWarnings({ "rawtypes""unchecked" })
370     private <T> T getMergedOrDefaultAnnotationValue(String attribute, Class annotationType, Class<T> targetType) {
371
372         Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationType);
373         if (annotation == null) {
374             return targetType.cast(AnnotationUtils.getDefaultValue(annotationType, attribute));
375         }
376
377         return targetType.cast(AnnotationUtils.getValue(annotation, attribute));
378     }
379
380     /*
381      * (non-Javadoc)
382      * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method)
383      */

384     @Override
385     protected JpaParameters createParameters(Method method) {
386         return new JpaParameters(method);
387     }
388
389     /*
390      * (non-Javadoc)
391      * @see org.springframework.data.repository.query.QueryMethod#getParameters()
392      */

393     @Override
394     public JpaParameters getParameters() {
395         return (JpaParameters) super.getParameters();
396     }
397
398     /*
399      * (non-Javadoc)
400      * @see org.springframework.data.repository.query.QueryMethod#isCollectionQuery()
401      */

402     @Override
403     public boolean isCollectionQuery() {
404         return this.isCollectionQuery.get();
405     }
406
407     /**
408      * Return {@literal trueif the method contains a {@link Procedure} annotation.
409      *
410      * @return
411      */

412     public boolean isProcedureQuery() {
413         return this.isProcedureQuery.get();
414     }
415
416     /**
417      * Returns a new {@link StoredProcedureAttributes} representing the stored procedure meta-data for this
418      * {@link JpaQueryMethod}.
419      *
420      * @return
421      */

422     StoredProcedureAttributes getProcedureAttributes() {
423
424         if (storedProcedureAttributes == null) {
425             this.storedProcedureAttributes = storedProcedureAttributeSource.createFrom(method, getEntityInformation());
426         }
427
428         return storedProcedureAttributes;
429     }
430 }
431