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 static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*;
19
20 import java.lang.reflect.Proxy;
21 import java.util.Collections;
22 import java.util.Date;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.function.Function;
27
28 import javax.persistence.Parameter;
29 import javax.persistence.Query;
30 import javax.persistence.TemporalType;
31 import javax.persistence.criteria.ParameterExpression;
32
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 import org.springframework.lang.Nullable;
37 import org.springframework.util.Assert;
38
39 /**
40  * The interface encapsulates the setting of query parameters which might use a significant number of variations of
41  * {@literal Query.setParameter}.
42  *
43  * @author Jens Schauder
44  * @author Mark Paluch
45  * @since 2.0
46  */

47 interface QueryParameterSetter {
48
49     void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling);
50
51     /** Noop implementation */
52     QueryParameterSetter NOOP = (query, values, errorHandling) -> {};
53
54     /**
55      * {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified.
56      */

57     class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter {
58
59         private final Function<JpaParametersParameterAccessor, Object> valueExtractor;
60         private final Parameter<?> parameter;
61         private final @Nullable TemporalType temporalType;
62
63         /**
64          * @param valueExtractor must not be {@literal null}.
65          * @param parameter must not be {@literal null}.
66          * @param temporalType may be {@literal null}.
67          */

68         NamedOrIndexedQueryParameterSetter(Function<JpaParametersParameterAccessor, Object> valueExtractor,
69                 Parameter<?> parameter, @Nullable TemporalType temporalType) {
70
71             Assert.notNull(valueExtractor, "ValueExtractor must not be null!");
72
73             this.valueExtractor = valueExtractor;
74             this.parameter = parameter;
75             this.temporalType = temporalType;
76         }
77
78         /*
79          * (non-Javadoc)
80          * @see org.springframework.data.jpa.repository.query.QueryParameterSetter#setParameter(javax.persistence.Query, java.lang.Object[])
81          */

82         @SuppressWarnings("unchecked")
83         @Override
84         public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor,
85                 ErrorHandling errorHandling) {
86
87             Object value = valueExtractor.apply(accessor);
88
89             if (temporalType != null) {
90
91                 // One would think we can simply use parameter to identify the parameter we want to set.
92                 // But that does not work with list valued parameters. At least Hibernate tries to bind them by name.
93                 // TODO: move to using setParameter(Parameter, value) when https://hibernate.atlassian.net/browse/HHH-11870 is
94                 // fixed.
95
96                 if (parameter instanceof ParameterExpression) {
97                     errorHandling.execute(() -> query.setParameter((Parameter<Date>) parameter, (Date) value, temporalType));
98                 } else if (query.hasNamedParameters() && parameter.getName() != null) {
99                     errorHandling.execute(() -> query.setParameter(parameter.getName(), (Date) value, temporalType));
100                 } else {
101
102                     Integer position = parameter.getPosition();
103
104                     if (position != null //
105                             && (query.getParameters().size() >= parameter.getPosition() //
106                                     || query.registerExcessParameters() //
107                                     || errorHandling == LENIENT)) {
108
109                         errorHandling.execute(() -> query.setParameter(parameter.getPosition(), (Date) value, temporalType));
110                     }
111                 }
112
113             } else {
114
115                 if (parameter instanceof ParameterExpression) {
116                     errorHandling.execute(() -> query.setParameter((Parameter<Object>) parameter, value));
117                 } else if (query.hasNamedParameters() && parameter.getName() != null) {
118                     errorHandling.execute(() -> query.setParameter(parameter.getName(), value));
119
120                 } else {
121
122                     Integer position = parameter.getPosition();
123
124                     if (position != null //
125                             && (query.getParameters().size() >= position //
126                                     || errorHandling == LENIENT //
127                                     || query.registerExcessParameters())) {
128                         errorHandling.execute(() -> query.setParameter(position, value));
129                     }
130                 }
131             }
132         }
133     }
134
135     enum ErrorHandling {
136
137         STRICT {
138
139             @Override
140             public void execute(Runnable block) {
141                 block.run();
142             }
143         },
144
145         LENIENT {
146
147             @Override
148             public void execute(Runnable block) {
149
150                 try {
151                     block.run();
152                 } catch (RuntimeException rex) {
153                     LOG.info("Silently ignoring", rex);
154                 }
155             }
156         };
157
158         private static final Logger LOG = LoggerFactory.getLogger(ErrorHandling.class);
159
160         abstract void execute(Runnable block);
161     }
162
163     /**
164      * Cache for {@link QueryMetadata}. Optimizes for small cache sizes on a best-effort basis.
165      */

166     class QueryMetadataCache {
167
168         private Map<String, QueryMetadata> cache = Collections.emptyMap();
169
170         /**
171          * Retrieve the {@link QueryMetadata} for a given {@code cacheKey}.
172          *
173          * @param cacheKey
174          * @param query
175          * @return
176          */

177         public QueryMetadata getMetadata(String cacheKey, Query query) {
178
179             QueryMetadata queryMetadata = cache.get(cacheKey);
180
181             if (queryMetadata == null) {
182
183                 queryMetadata = new QueryMetadata(query);
184
185                 Map<String, QueryMetadata> cache;
186
187                 if (this.cache.isEmpty()) {
188                     cache = Collections.singletonMap(cacheKey, queryMetadata);
189                 } else {
190                     cache = new HashMap<>(this.cache);
191                     cache.put(cacheKey, queryMetadata);
192                 }
193
194                 synchronized (this) {
195                     this.cache = cache;
196                 }
197             }
198
199             return queryMetadata;
200         }
201     }
202
203     /**
204      * Metadata for a JPA {@link Query}.
205      */

206     class QueryMetadata {
207
208         private final boolean namedParameters;
209         private final Set<Parameter<?>> parameters;
210         private final boolean registerExcessParameters;
211
212         QueryMetadata(Query query) {
213
214             this.namedParameters = QueryUtils.hasNamedParameter(query);
215             this.parameters = query.getParameters();
216
217             // DATAJPA-1172
218             // Since EclipseLink doesn't reliably report whether a query has parameters
219             // we simply try to set the parameters and ignore possible failures.
220             // this is relevant for native queries with SpEL expressions, where the method parameters don't have to match the
221             // parameters in the query.
222             // https://bugs.eclipse.org/bugs/show_bug.cgi?id=521915
223
224             this.registerExcessParameters = query.getParameters().size() == 0
225                     && unwrapClass(query).getName().startsWith("org.eclipse");
226         }
227
228         QueryMetadata(QueryMetadata metadata) {
229
230             this.namedParameters = metadata.namedParameters;
231             this.parameters = metadata.parameters;
232             this.registerExcessParameters = metadata.registerExcessParameters;
233         }
234
235         /**
236          * Create a {@link BindableQuery} for a {@link Query}.
237          *
238          * @param query
239          * @return
240          */

241         public BindableQuery withQuery(Query query) {
242             return new BindableQuery(this, query);
243         }
244
245         /**
246          * @return
247          */

248         public Set<Parameter<?>> getParameters() {
249             return parameters;
250         }
251
252         /**
253          * @return {@literal trueif the underlying query uses named parameters.
254          */

255         public boolean hasNamedParameters() {
256             return this.namedParameters;
257         }
258
259         public boolean registerExcessParameters() {
260             return this.registerExcessParameters;
261         }
262
263         /**
264          * Returns the actual target {@link Query} instance, even if the provided query is a {@link Proxy} based on
265          * {@link org.springframework.orm.jpa.SharedEntityManagerCreator.DeferredQueryInvocationHandler}.
266          *
267          * @param query a {@link Query} instance, possibly a Proxy.
268          * @return the class of the actual underlying class if it can be determined, the class of the passed in instance
269          *         otherwise.
270          */

271         private static Class<?> unwrapClass(Query query) {
272
273             Class<? extends Query> queryType = query.getClass();
274
275             try {
276
277                 return Proxy.isProxyClass(queryType) //
278                         ? query.unwrap(null).getClass() //
279                         : queryType;
280
281             } catch (RuntimeException e) {
282
283                 LoggerFactory.getLogger(QueryMetadata.class).warn("Failed to unwrap actual class for Query proxy.", e);
284
285                 return queryType;
286             }
287         }
288     }
289
290     /**
291      * A bindable {@link Query}.
292      */

293     class BindableQuery extends QueryMetadata {
294
295         private final Query query;
296         private final Query unwrapped;
297
298         BindableQuery(QueryMetadata metadata, Query query) {
299             super(metadata);
300             this.query = query;
301             this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query;
302         }
303
304         private BindableQuery(Query query) {
305             super(query);
306             this.query = query;
307             this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query;
308         }
309
310         public static BindableQuery from(Query query) {
311             return new BindableQuery(query);
312         }
313
314         public Query getQuery() {
315             return query;
316         }
317
318         public <T> Query setParameter(Parameter<T> param, T value) {
319             return unwrapped.setParameter(param, value);
320         }
321
322         public Query setParameter(Parameter<Date> param, Date value, TemporalType temporalType) {
323             return unwrapped.setParameter(param, value, temporalType);
324         }
325
326         public Query setParameter(String name, Object value) {
327             return unwrapped.setParameter(name, value);
328         }
329
330         public Query setParameter(String name, Date value, TemporalType temporalType) {
331             return query.setParameter(name, value, temporalType);
332         }
333
334         public Query setParameter(int position, Object value) {
335             return unwrapped.setParameter(position, value);
336         }
337
338         public Query setParameter(int position, Date value, TemporalType temporalType) {
339             return unwrapped.setParameter(position, value, temporalType);
340         }
341     }
342 }
343