1 /*
2  * Copyright 2017-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 import java.util.function.Function;
20
21 import javax.persistence.Query;
22 import javax.persistence.TemporalType;
23
24 import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
25 import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
26 import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter;
27 import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
28 import org.springframework.data.repository.query.Parameter;
29 import org.springframework.data.repository.query.Parameters;
30 import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
31 import org.springframework.data.spel.EvaluationContextProvider;
32 import org.springframework.expression.EvaluationContext;
33 import org.springframework.expression.Expression;
34 import org.springframework.expression.spel.standard.SpelExpressionParser;
35 import org.springframework.lang.Nullable;
36 import org.springframework.util.Assert;
37
38 /**
39  * Encapsulates different strategies for the creation of a {@link QueryParameterSetter} from a {@link Query} and a
40  * {@link ParameterBinding}
41  *
42  * @author Jens Schauder
43  * @author Oliver Gierke
44  * @author Mark Paluch
45  * @since 2.0
46  */

47 abstract class QueryParameterSetterFactory {
48
49     @Nullable
50     abstract QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery);
51
52     /**
53      * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}.
54      *
55      * @param parameters must not be {@literal null}.
56      * @return a basic {@link QueryParameterSetterFactory} that can handle named and index parameters.
57      */

58     static QueryParameterSetterFactory basic(JpaParameters parameters) {
59
60         Assert.notNull(parameters, "JpaParameters must not be null!");
61
62         return new BasicQueryParameterSetterFactory(parameters);
63     }
64
65     /**
66      * Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters} and
67      * {@link ParameterMetadata}.
68      *
69      * @param parameters must not be {@literal null}.
70      * @param metadata must not be {@literal null}.
71      * @return a {@link QueryParameterSetterFactory} for criteria Queries.
72      */

73     static QueryParameterSetterFactory forCriteriaQuery(JpaParameters parameters, List<ParameterMetadata<?>> metadata) {
74
75         Assert.notNull(parameters, "JpaParameters must not be null!");
76         Assert.notNull(metadata, "ParameterMetadata must not be null!");
77
78         return new CriteriaQueryParameterSetterFactory(parameters, metadata);
79     }
80
81     /**
82      * Creates a new {@link QueryParameterSetterFactory} for the given {@link SpelExpressionParser},
83      * {@link EvaluationContextProvider} and {@link Parameters}.
84      *
85      * @param parser must not be {@literal null}.
86      * @param evaluationContextProvider must not be {@literal null}.
87      * @param parameters must not be {@literal null}.
88      * @return a {@link QueryParameterSetterFactory} that can handle
89      *         {@link org.springframework.expression.spel.standard.SpelExpression}s.
90      */

91     static QueryParameterSetterFactory parsing(SpelExpressionParser parser,
92             QueryMethodEvaluationContextProvider evaluationContextProvider, Parameters<?, ?> parameters) {
93
94         Assert.notNull(parser, "SpelExpressionParser must not be null!");
95         Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
96         Assert.notNull(parameters, "Parameters must not be null!");
97
98         return new ExpressionBasedQueryParameterSetterFactory(parser, evaluationContextProvider, parameters);
99     }
100
101     /**
102      * Creates a {@link QueryParameterSetter} from a {@link JpaParameter}. Handles named and indexed parameters,
103      * TemporalType annotations and might ignore certain exception when requested to do so.
104      *
105      * @param valueExtractor extracts the relevant value from an array of method parameter values.
106      * @param binding the binding of the query parameter to be set.
107      * @param parameter the method parameter to bind.
108      */

109     private static QueryParameterSetter createSetter(Function<JpaParametersParameterAccessor, Object> valueExtractor,
110             ParameterBinding binding, @Nullable JpaParameter parameter) {
111
112         TemporalType temporalType = parameter != null && parameter.isTemporalParameter() //
113                 ? parameter.getRequiredTemporalType() //
114                 : null;
115
116         return new NamedOrIndexedQueryParameterSetter(valueExtractor.andThen(binding::prepare),
117                 ParameterImpl.of(parameter, binding), temporalType);
118     }
119
120     /**
121      * Handles bindings that are SpEL expressions by evaluating the expression to obtain a value.
122      *
123      * @author Jens Schauder
124      * @author Oliver Gierke
125      * @since 2.0
126      */

127     private static class ExpressionBasedQueryParameterSetterFactory extends QueryParameterSetterFactory {
128
129         private final SpelExpressionParser parser;
130         private final QueryMethodEvaluationContextProvider evaluationContextProvider;
131         private final Parameters<?, ?> parameters;
132
133         /**
134          * @param parser must not be {@literal null}.
135          * @param evaluationContextProvider must not be {@literal null}.
136          * @param parameters must not be {@literal null}.
137          */

138         ExpressionBasedQueryParameterSetterFactory(SpelExpressionParser parser,
139                 QueryMethodEvaluationContextProvider evaluationContextProvider, Parameters<?, ?> parameters) {
140
141             Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
142             Assert.notNull(parser, "SpelExpressionParser must not be null!");
143             Assert.notNull(parameters, "Parameters must not be null!");
144
145             this.evaluationContextProvider = evaluationContextProvider;
146             this.parser = parser;
147             this.parameters = parameters;
148         }
149
150         /*
151          * (non-Javadoc)
152          * @see org.springframework.data.jpa.repository.query.QueryParameterSetterFactory#create(org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding, java.lang.String)
153          */

154         @Nullable
155         @Override
156         public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) {
157
158             if (!binding.isExpression()) {
159                 return null;
160             }
161
162             Expression expression = parser.parseExpression(binding.getExpression());
163
164             return createSetter(values -> evaluateExpression(expression, values), binding, null);
165         }
166
167         /**
168          * Evaluates the given {@link Expression} against the given values.
169          *
170          * @param expression must not be {@literal null}.
171          * @param values must not be {@literal null}.
172          * @return the result of the evaluation.
173          */

174         @Nullable
175         private Object evaluateExpression(Expression expression, JpaParametersParameterAccessor accessor) {
176
177             EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameters, accessor.getValues());
178
179             return expression.getValue(context, Object.class);
180         }
181     }
182
183     /**
184      * Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters.
185      *
186      * @author Jens Schauder
187      * @author Oliver Gierke
188      * @since 2.0
189      */

190     private static class BasicQueryParameterSetterFactory extends QueryParameterSetterFactory {
191
192         private final JpaParameters parameters;
193
194         /**
195          * @param parameters must not be {@literal null}.
196          */

197         BasicQueryParameterSetterFactory(JpaParameters parameters) {
198
199             Assert.notNull(parameters, "JpaParameters must not be null!");
200
201             this.parameters = parameters;
202         }
203
204         /*
205          * (non-Javadoc)
206          * @see org.springframework.data.jpa.repository.query.QueryParameterSetterFactory#create(org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding, java.lang.String)
207          */

208         @Override
209         public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) {
210
211             Assert.notNull(binding, "Binding must not be null.");
212
213             JpaParameter parameter;
214
215             if (declaredQuery.hasNamedParameter()) {
216                 parameter = findParameterForBinding(binding);
217             } else {
218
219                 int parameterIndex = binding.getRequiredPosition() - 1;
220                 JpaParameters bindableParameters = parameters.getBindableParameters();
221
222                 Assert.isTrue( //
223                         parameterIndex < bindableParameters.getNumberOfParameters(), //
224                         () -> String.format( //
225                                 "At least %s parameter(s) provided but only %s parameter(s) present in query."//
226                                 binding.getRequiredPosition(), //
227                                 bindableParameters.getNumberOfParameters() //
228                         ) //
229                 );
230
231                 parameter = bindableParameters.getParameter(binding.getRequiredPosition() - 1);
232             }
233
234             return parameter == null //
235                     ? QueryParameterSetter.NOOP //
236                     : createSetter(values -> getValue(values, parameter), binding, parameter);
237         }
238
239         @Nullable
240         private JpaParameter findParameterForBinding(ParameterBinding binding) {
241
242             JpaParameters bindableParameters = parameters.getBindableParameters();
243
244             for (JpaParameter bindableParameter : bindableParameters) {
245                 if (binding.getRequiredName().equals(getName(bindableParameter))) {
246                     return bindableParameter;
247                 }
248             }
249
250             return null;
251         }
252
253         private Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) {
254             return accessor.getValue(parameter);
255         }
256
257         private static String getName(JpaParameter p) {
258             return p.getName().orElseThrow(() -> new IllegalStateException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED));
259         }
260     }
261
262     /**
263      * @author Jens Schauder
264      * @author Oliver Gierke
265      * @see QueryParameterSetterFactory
266      */

267     private static class CriteriaQueryParameterSetterFactory extends QueryParameterSetterFactory {
268
269         private final JpaParameters parameters;
270         private final List<ParameterMetadata<?>> expressions;
271
272         /**
273          * Creates a new {@link QueryParameterSetterFactory} from the given {@link JpaParameters} and
274          * {@link ParameterMetadata}.
275          *
276          * @param parameters must not be {@literal null}.
277          * @param metadata must not be {@literal null}.
278          */

279         CriteriaQueryParameterSetterFactory(JpaParameters parameters, List<ParameterMetadata<?>> metadata) {
280
281             Assert.notNull(parameters, "JpaParameters must not be null!");
282             Assert.notNull(metadata, "Expressions must not be null!");
283
284             this.parameters = parameters;
285             this.expressions = metadata;
286         }
287
288         /*
289          * (non-Javadoc)
290          * @see org.springframework.data.jpa.repository.query.QueryParameterSetterFactory#create(org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding, java.lang.String)
291          */

292         @Override
293         public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) {
294
295             int parameterIndex = binding.getRequiredPosition() - 1;
296
297             Assert.isTrue( //
298                     parameterIndex < expressions.size(), //
299                     () -> String.format( //
300                             "At least %s parameter(s) provided but only %s parameter(s) present in query."//
301                             binding.getRequiredPosition(), //
302                             expressions.size() //
303                     ) //
304             );
305
306             ParameterMetadata<?> metadata = expressions.get(parameterIndex);
307
308             if (metadata.isIsNullParameter()) {
309                 return QueryParameterSetter.NOOP;
310             }
311
312             JpaParameter parameter = parameters.getBindableParameter(parameterIndex);
313             TemporalType temporalType = parameter.isTemporalParameter() ? parameter.getRequiredTemporalType() : null;
314
315             return new NamedOrIndexedQueryParameterSetter(values -> {
316                 return getAndPrepare(parameter, metadata, values);
317             }, metadata.getExpression(), temporalType);
318         }
319
320         @Nullable
321         private Object getAndPrepare(JpaParameter parameter, ParameterMetadata<?> metadata,
322                 JpaParametersParameterAccessor accessor) {
323             return metadata.prepare(accessor.getValue(parameter));
324         }
325     }
326
327     private static class ParameterImpl<T> implements javax.persistence.Parameter<T> {
328
329         private final Class<T> parameterType;
330         private final @Nullable String name;
331         private final @Nullable Integer position;
332
333         /**
334          * Creates a new {@link ParameterImpl} for the given {@link JpaParameter} and {@link ParameterBinding}.
335          *
336          * @param parameter can be {@literal null}.
337          * @param binding must not be {@literal null}.
338          * @return a {@link javax.persistence.Parameter} object based on the information from the arguments.
339          */

340         static javax.persistence.Parameter<?> of(@Nullable JpaParameter parameter, ParameterBinding binding) {
341
342             Class<?> type = parameter == null ? Object.class : parameter.getType();
343
344             return new ParameterImpl<>(type, getName(parameter, binding), binding.getPosition());
345         }
346
347         /**
348          * Creates a new {@link ParameterImpl} for the given name, position and parameter type.
349          *
350          * @param parameterType must not be {@literal null}.
351          * @param name can be {@literal null}.
352          * @param position can be {@literal null}.
353          */

354         private ParameterImpl(Class<T> parameterType, @Nullable String name, @Nullable Integer position) {
355
356             this.name = name;
357             this.position = position;
358             this.parameterType = parameterType;
359         }
360
361         /*
362          * (non-Javadoc)
363          * @see javax.persistence.Parameter#getName()
364          */

365         @Nullable
366         @Override
367         public String getName() {
368             return name;
369         }
370
371         /*
372          * (non-Javadoc)
373          * @see javax.persistence.Parameter#getPosition()
374          */

375         @Nullable
376         @Override
377         public Integer getPosition() {
378             return position;
379         }
380
381         /*
382          * (non-Javadoc)
383          * @see javax.persistence.Parameter#getParameterType()
384          */

385         @Override
386         public Class<T> getParameterType() {
387             return parameterType;
388         }
389
390         @Nullable
391         private static String getName(@Nullable JpaParameter parameter, ParameterBinding binding) {
392
393             if (parameter == null) {
394                 return binding.getName();
395             }
396
397             return parameter.isNamedParameter() //
398                     ? parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O parameter needs to have a name!")) //
399                     : null;
400         }
401     }
402 }
403