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.util.List;
19
20 import javax.persistence.EntityManager;
21 import javax.persistence.Query;
22 import javax.persistence.TypedQuery;
23 import javax.persistence.criteria.CriteriaBuilder;
24 import javax.persistence.criteria.CriteriaQuery;
25
26 import org.springframework.data.domain.Sort;
27 import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
28 import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution;
29 import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution;
30 import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
31 import org.springframework.data.repository.query.ResultProcessor;
32 import org.springframework.data.repository.query.ReturnedType;
33 import org.springframework.data.repository.query.parser.Part;
34 import org.springframework.data.repository.query.parser.Part.Type;
35 import org.springframework.data.repository.query.parser.PartTree;
36 import org.springframework.data.util.Streamable;
37 import org.springframework.lang.Nullable;
38
39 /**
40  * A {@link AbstractJpaQuery} implementation based on a {@link PartTree}.
41  *
42  * @author Oliver Gierke
43  * @author Thomas Darimont
44  * @author Christoph Strobl
45  * @author Jens Schauder
46  * @author Mark Paluch
47  * @author Сергей Цыпанов
48  */

49 public class PartTreeJpaQuery extends AbstractJpaQuery {
50
51     private final PartTree tree;
52     private final JpaParameters parameters;
53
54     private final QueryPreparer query;
55     private final QueryPreparer countQuery;
56     private final EntityManager em;
57     private final EscapeCharacter escape;
58
59     /**
60      * Creates a new {@link PartTreeJpaQuery}.
61      *
62      * @param method must not be {@literal null}.
63      * @param em must not be {@literal null}.
64      */

65     PartTreeJpaQuery(JpaQueryMethod method, EntityManager em) {
66         this(method, em, EscapeCharacter.DEFAULT);
67     }
68
69     /**
70      * Creates a new {@link PartTreeJpaQuery}.
71      *
72      * @param method must not be {@literal null}.
73      * @param em must not be {@literal null}.
74      * @param escape character used for escaping characters used as patterns in LIKE-expressions.
75      */

76     PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, EscapeCharacter escape) {
77
78         super(method, em);
79
80         this.em = em;
81         this.escape = escape;
82         Class<?> domainClass = method.getEntityInformation().getJavaType();
83         this.parameters = method.getParameters();
84
85         boolean recreationRequired = parameters.hasDynamicProjection() || parameters.potentiallySortsDynamically();
86
87         try {
88
89             this.tree = new PartTree(method.getName(), domainClass);
90             validate(tree, parameters, method.toString());
91             this.countQuery = new CountQueryPreparer(recreationRequired);
92             this.query = tree.isCountProjection() ? countQuery : new QueryPreparer(recreationRequired);
93
94         } catch (Exception o_O) {
95             throw new IllegalArgumentException(
96                     String.format("Failed to create query for method %s! %s", method, o_O.getMessage()), o_O);
97         }
98     }
99
100     /*
101      * (non-Javadoc)
102      * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateQuery(JpaParametersParameterAccessor)
103      */

104     @Override
105     public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
106         return query.createQuery(accessor);
107     }
108
109     /*
110      * (non-Javadoc)
111      * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(JpaParametersParameterAccessor)
112      */

113     @Override
114     @SuppressWarnings("unchecked")
115     public TypedQuery<Long> doCreateCountQuery(JpaParametersParameterAccessor accessor) {
116         return (TypedQuery<Long>) countQuery.createQuery(accessor);
117     }
118
119     /*
120      * (non-Javadoc)
121      * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#getExecution()
122      */

123     @Override
124     protected JpaQueryExecution getExecution() {
125
126         if (this.tree.isDelete()) {
127             return new DeleteExecution(em);
128         } else if (this.tree.isExistsProjection()) {
129             return new ExistsExecution();
130         }
131
132         return super.getExecution();
133     }
134
135     private static void validate(PartTree tree, JpaParameters parameters, String methodName) {
136
137         int argCount = 0;
138
139         Iterable<Part> parts = () -> tree.stream().flatMap(Streamable::stream).iterator();
140
141         for (Part part : parts) {
142
143             int numberOfArguments = part.getNumberOfArguments();
144
145             for (int i = 0; i < numberOfArguments; i++) {
146
147                 throwExceptionOnArgumentMismatch(methodName, part, parameters, argCount);
148
149                 argCount++;
150             }
151         }
152     }
153
154     private static void throwExceptionOnArgumentMismatch(String methodName, Part part, JpaParameters parameters,
155             int index) {
156
157         Type type = part.getType();
158         String property = part.getProperty().toDotPath();
159
160         if (!parameters.getBindableParameters().hasParameterAt(index)) {
161             throw new IllegalStateException(String.format(
162                     "Method %s expects at least %d arguments but only found %d. This leaves an operator of type %s for property %s unbound.",
163                     methodName, index + 1, index, type.name(), property));
164         }
165
166         JpaParameter parameter = parameters.getBindableParameter(index);
167
168         if (expectsCollection(type) && !parameterIsCollectionLike(parameter)) {
169             throw new IllegalStateException(wrongParameterTypeMessage(methodName, property, type, "Collection", parameter));
170         } else if (!expectsCollection(type) && !parameterIsScalarLike(parameter)) {
171             throw new IllegalStateException(wrongParameterTypeMessage(methodName, property, type, "scalar", parameter));
172         }
173     }
174
175     private static String wrongParameterTypeMessage(String methodName, String property, Type operatorType,
176             String expectedArgumentType, JpaParameter parameter) {
177
178         return String.format("Operator %s on %s requires a %s argument, found %s in method %s.", operatorType.name(),
179                 property, expectedArgumentType, parameter.getType(), methodName);
180     }
181
182     private static boolean parameterIsCollectionLike(JpaParameter parameter) {
183         return Iterable.class.isAssignableFrom(parameter.getType()) || parameter.getType().isArray();
184     }
185
186     /**
187      * Arrays are may be treated as collection like or in the case of binary data as scalar
188      */

189     private static boolean parameterIsScalarLike(JpaParameter parameter) {
190         return !Iterable.class.isAssignableFrom(parameter.getType());
191     }
192
193     private static boolean expectsCollection(Type type) {
194         return type == Type.IN || type == Type.NOT_IN;
195     }
196
197     /**
198      * Query preparer to create {@link CriteriaQuery} instances and potentially cache them.
199      *
200      * @author Oliver Gierke
201      * @author Thomas Darimont
202      */

203     private class QueryPreparer {
204
205         private final @Nullable CriteriaQuery<?> cachedCriteriaQuery;
206         private final @Nullable ParameterBinder cachedParameterBinder;
207         private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
208
209         QueryPreparer(boolean recreateQueries) {
210
211             JpaQueryCreator creator = createCreator(null);
212
213             if (recreateQueries) {
214                 this.cachedCriteriaQuery = null;
215                 this.cachedParameterBinder = null;
216             } else {
217                 this.cachedCriteriaQuery = creator.createQuery();
218                 this.cachedParameterBinder = getBinder(creator.getParameterExpressions());
219             }
220         }
221
222         /**
223          * Creates a new {@link Query} for the given parameter values.
224          */

225         public Query createQuery(JpaParametersParameterAccessor accessor) {
226
227             CriteriaQuery<?> criteriaQuery = cachedCriteriaQuery;
228             ParameterBinder parameterBinder = cachedParameterBinder;
229
230             if (cachedCriteriaQuery == null || accessor.hasBindableNullValue()) {
231                 JpaQueryCreator creator = createCreator(accessor);
232                 criteriaQuery = creator.createQuery(getDynamicSort(accessor));
233                 List<ParameterMetadata<?>> expressions = creator.getParameterExpressions();
234                 parameterBinder = getBinder(expressions);
235             }
236
237             if (parameterBinder == null) {
238                 throw new IllegalStateException("ParameterBinder is null!");
239             }
240
241             TypedQuery<?> query = createQuery(criteriaQuery);
242
243             return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, query, accessor, this.metadataCache));
244         }
245
246         /**
247          * Restricts the max results of the given {@link Query} if the current {@code tree} marks this {@code query} as
248          * limited.
249          */

250         @SuppressWarnings("ConstantConditions")
251         private Query restrictMaxResultsIfNecessary(Query query) {
252
253             if (tree.isLimiting()) {
254
255                 if (query.getMaxResults() != Integer.MAX_VALUE) {
256                     /*
257                      * In order to return the correct results, we have to adjust the first result offset to be returned if:
258                      * - a Pageable parameter is present
259                      * - AND the requested page number > 0
260                      * - AND the requested page size was bigger than the derived result limitation via the First/Top keyword.
261                      */

262                     if (query.getMaxResults() > tree.getMaxResults() && query.getFirstResult() > 0) {
263                         query.setFirstResult(query.getFirstResult() - (query.getMaxResults() - tree.getMaxResults()));
264                     }
265                 }
266
267                 query.setMaxResults(tree.getMaxResults());
268             }
269
270             if (tree.isExistsProjection()) {
271                 query.setMaxResults(1);
272             }
273
274             return query;
275         }
276
277         /**
278          * Checks whether we are working with a cached {@link CriteriaQuery} and synchronizes the creation of a
279          * {@link TypedQuery} instance from it. This is due to non-thread-safety in the {@link CriteriaQuery} implementation
280          * of some persistence providers (i.e. Hibernate in this case), see DATAJPA-396.
281          *
282          * @param criteriaQuery must not be {@literal null}.
283          */

284         private TypedQuery<?> createQuery(CriteriaQuery<?> criteriaQuery) {
285
286             if (this.cachedCriteriaQuery != null) {
287                 synchronized (this.cachedCriteriaQuery) {
288                     return getEntityManager().createQuery(criteriaQuery);
289                 }
290             }
291
292             return getEntityManager().createQuery(criteriaQuery);
293         }
294
295         protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) {
296
297             EntityManager entityManager = getEntityManager();
298
299             CriteriaBuilder builder = entityManager.getCriteriaBuilder();
300             ResultProcessor processor = getQueryMethod().getResultProcessor();
301
302             ParameterMetadataProvider provider;
303             ReturnedType returnedType;
304
305             if (accessor != null) {
306                 provider = new ParameterMetadataProvider(builder, accessor, escape);
307                 returnedType = processor.withDynamicProjection(accessor).getReturnedType();
308             } else {
309                 provider = new ParameterMetadataProvider(builder, parameters, escape);
310                 returnedType = processor.getReturnedType();
311             }
312
313             return new JpaQueryCreator(tree, returnedType, builder, provider);
314         }
315
316         /**
317          * Invokes parameter binding on the given {@link TypedQuery}.
318          */

319         protected Query invokeBinding(ParameterBinder binder, TypedQuery<?> query, JpaParametersParameterAccessor accessor,
320                 QueryParameterSetter.QueryMetadataCache metadataCache) {
321
322             QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("query", query);
323
324             return binder.bindAndPrepare(query, metadata, accessor);
325         }
326
327         private ParameterBinder getBinder(List<ParameterMetadata<?>> expressions) {
328             return ParameterBinderFactory.createCriteriaBinder(parameters, expressions);
329         }
330
331         private Sort getDynamicSort(JpaParametersParameterAccessor accessor) {
332
333             return parameters.potentiallySortsDynamically() //
334                     ? accessor.getSort() //
335                     : Sort.unsorted();
336         }
337     }
338
339     /**
340      * Special {@link QueryPreparer} to create count queries.
341      *
342      * @author Oliver Gierke
343      * @author Thomas Darimont
344      */

345     private class CountQueryPreparer extends QueryPreparer {
346
347         CountQueryPreparer(boolean recreateQueries) {
348             super(recreateQueries);
349         }
350
351         @Override
352         protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) {
353
354             EntityManager entityManager = getEntityManager();
355             CriteriaBuilder builder = entityManager.getCriteriaBuilder();
356
357             ParameterMetadataProvider provider;
358
359             if (accessor != null) {
360                 provider = new ParameterMetadataProvider(builder, accessor, escape);
361             } else {
362                 provider = new ParameterMetadataProvider(builder, parameters, escape);
363             }
364
365             return new JpaCountQueryCreator(tree, getQueryMethod().getResultProcessor().getReturnedType(), builder, provider);
366         }
367
368         /**
369          * Customizes binding by skipping the pagination.
370          */

371         @Override
372         protected Query invokeBinding(ParameterBinder binder, TypedQuery<?> query, JpaParametersParameterAccessor accessor,
373                 QueryParameterSetter.QueryMetadataCache metadataCache) {
374
375             QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("countquery", query);
376
377             return binder.bind(query, metadata, accessor);
378         }
379     }
380 }
381