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.Arrays;
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.stream.Collectors;
25
26 import javax.persistence.EntityManager;
27 import javax.persistence.LockModeType;
28 import javax.persistence.Query;
29 import javax.persistence.QueryHint;
30 import javax.persistence.Tuple;
31 import javax.persistence.TupleElement;
32 import javax.persistence.TypedQuery;
33
34 import org.springframework.core.convert.converter.Converter;
35 import org.springframework.data.jpa.provider.PersistenceProvider;
36 import org.springframework.data.jpa.repository.EntityGraph;
37 import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution;
38 import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution;
39 import org.springframework.data.jpa.repository.query.JpaQueryExecution.PagedExecution;
40 import org.springframework.data.jpa.repository.query.JpaQueryExecution.ProcedureExecution;
41 import org.springframework.data.jpa.repository.query.JpaQueryExecution.SingleEntityExecution;
42 import org.springframework.data.jpa.repository.query.JpaQueryExecution.SlicedExecution;
43 import org.springframework.data.jpa.repository.query.JpaQueryExecution.StreamExecution;
44 import org.springframework.data.jpa.util.JpaMetamodel;
45 import org.springframework.data.repository.query.RepositoryQuery;
46 import org.springframework.data.repository.query.ResultProcessor;
47 import org.springframework.data.repository.query.ReturnedType;
48 import org.springframework.data.util.Lazy;
49 import org.springframework.lang.Nullable;
50 import org.springframework.util.Assert;
51
52 /**
53  * Abstract base class to implement {@link RepositoryQuery}s.
54  *
55  * @author Oliver Gierke
56  * @author Thomas Darimont
57  * @author Mark Paluch
58  * @author Christoph Strobl
59  * @author Nicolas Cirigliano
60  * @author Jens Schauder
61  * @author Сергей Цыпанов
62  */

63 public abstract class AbstractJpaQuery implements RepositoryQuery {
64
65     private final JpaQueryMethod method;
66     private final EntityManager em;
67     private final JpaMetamodel metamodel;
68     private final PersistenceProvider provider;
69     private final Lazy<JpaQueryExecution> execution;
70
71     final Lazy<ParameterBinder> parameterBinder = new Lazy<>(this::createBinder);
72
73     /**
74      * Creates a new {@link AbstractJpaQuery} from the given {@link JpaQueryMethod}.
75      *
76      * @param method
77      * @param em
78      */

79     public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) {
80
81         Assert.notNull(method, "JpaQueryMethod must not be null!");
82         Assert.notNull(em, "EntityManager must not be null!");
83
84         this.method = method;
85         this.em = em;
86         this.metamodel = JpaMetamodel.of(em.getMetamodel());
87         this.provider = PersistenceProvider.fromEntityManager(em);
88         this.execution = Lazy.of(() -> {
89
90             if (method.isStreamQuery()) {
91                 return new StreamExecution();
92             } else if (method.isProcedureQuery()) {
93                 return new ProcedureExecution();
94             } else if (method.isCollectionQuery()) {
95                 return new CollectionExecution();
96             } else if (method.isSliceQuery()) {
97                 return new SlicedExecution();
98             } else if (method.isPageQuery()) {
99                 return new PagedExecution();
100             } else if (method.isModifyingQuery()) {
101                 return null;
102             } else {
103                 return new SingleEntityExecution();
104             }
105         });
106     }
107
108     /*
109      * (non-Javadoc)
110      * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
111      */

112     @Override
113     public JpaQueryMethod getQueryMethod() {
114         return method;
115     }
116
117     /**
118      * Returns the {@link EntityManager}.
119      *
120      * @return will never be {@literal null}.
121      */

122     protected EntityManager getEntityManager() {
123         return em;
124     }
125
126     /**
127      * Returns the {@link JpaMetamodel}.
128      *
129      * @return
130      */

131     protected JpaMetamodel getMetamodel() {
132         return metamodel;
133     }
134
135     /*
136      * (non-Javadoc)
137      * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
138      */

139     @Nullable
140     @Override
141     public Object execute(Object[] parameters) {
142         return doExecute(getExecution(), parameters);
143     }
144
145     /**
146      * @param execution
147      * @param values
148      * @return
149      */

150     @Nullable
151     private Object doExecute(JpaQueryExecution execution, Object[] values) {
152
153         JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(method.getParameters(), values);
154         Object result = execution.execute(this, accessor);
155
156         ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
157         return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType()));
158     }
159
160     protected JpaQueryExecution getExecution() {
161
162         JpaQueryExecution execution = this.execution.getNullable();
163
164         if (execution != null) {
165             return execution;
166         }
167
168         if (method.isModifyingQuery()) {
169             return new ModifyingExecution(method, em);
170         } else {
171             return new SingleEntityExecution();
172         }
173     }
174
175     /**
176      * Applies the declared query hints to the given query.
177      *
178      * @param query
179      * @return
180      */

181     protected <T extends Query> T applyHints(T query, JpaQueryMethod method) {
182
183         List<QueryHint> hints = method.getHints();
184
185         if (!hints.isEmpty()) {
186             for (QueryHint hint : hints) {
187                 applyQueryHint(query, hint);
188             }
189         }
190
191         return query;
192     }
193
194     /**
195      * Protected to be able to customize in sub-classes.
196      *
197      * @param query must not be {@literal null}.
198      * @param hint must not be {@literal null}.
199      */

200     protected <T extends Query> void applyQueryHint(T query, QueryHint hint) {
201
202         Assert.notNull(query, "Query must not be null!");
203         Assert.notNull(hint, "QueryHint must not be null!");
204
205         query.setHint(hint.name(), hint.value());
206     }
207
208     /**
209      * Applies the {@link LockModeType} provided by the {@link JpaQueryMethod} to the given {@link Query}.
210      *
211      * @param query must not be {@literal null}.
212      * @param method must not be {@literal null}.
213      * @return
214      */

215     private Query applyLockMode(Query query, JpaQueryMethod method) {
216
217         LockModeType lockModeType = method.getLockModeType();
218         return lockModeType == null ? query : query.setLockMode(lockModeType);
219     }
220
221     protected ParameterBinder createBinder() {
222         return ParameterBinderFactory.createBinder(getQueryMethod().getParameters());
223     }
224
225     protected Query createQuery(JpaParametersParameterAccessor parameters) {
226         return applyLockMode(applyEntityGraphConfiguration(applyHints(doCreateQuery(parameters), method), method), method);
227     }
228
229     /**
230      * Configures the {@link javax.persistence.EntityGraph} to use for the given {@link JpaQueryMethod} if the
231      * {@link EntityGraph} annotation is present.
232      *
233      * @param query must not be {@literal null}.
234      * @param method must not be {@literal null}.
235      * @return
236      */

237     private Query applyEntityGraphConfiguration(Query query, JpaQueryMethod method) {
238
239         JpaEntityGraph entityGraph = method.getEntityGraph();
240
241         if (entityGraph != null) {
242             Map<String, Object> hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph(),
243                     getQueryMethod().getEntityInformation().getJavaType());
244
245             for (Map.Entry<String, Object> hint : hints.entrySet()) {
246                 query.setHint(hint.getKey(), hint.getValue());
247             }
248         }
249
250         return query;
251     }
252
253     protected Query createCountQuery(JpaParametersParameterAccessor values) {
254         Query countQuery = doCreateCountQuery(values);
255         return method.applyHintsToCountQuery() ? applyHints(countQuery, method) : countQuery;
256     }
257
258     /**
259      * Returns the type to be used when creating the JPA query.
260      *
261      * @return
262      * @since 2.0.5
263      */

264     @Nullable
265     protected Class<?> getTypeToRead(ReturnedType returnedType) {
266
267         if (PersistenceProvider.ECLIPSELINK.equals(provider)) {
268             return null;
269         }
270
271         return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) //
272                 ? Tuple.class //
273                 : null;
274     }
275
276     /**
277      * Creates a {@link Query} instance for the given values.
278      *
279      * @param accessor must not be {@literal null}.
280      * @return
281      */

282     protected abstract Query doCreateQuery(JpaParametersParameterAccessor accessor);
283
284     /**
285      * Creates a {@link TypedQuery} for counting using the given values.
286      *
287      * @param accessor must not be {@literal null}.
288      * @return
289      */

290     protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor);
291
292     static class TupleConverter implements Converter<Object, Object> {
293
294         private final ReturnedType type;
295
296         /**
297          * Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
298          *
299          * @param type must not be {@literal null}.
300          */

301         public TupleConverter(ReturnedType type) {
302
303             Assert.notNull(type, "Returned type must not be null!");
304
305             this.type = type;
306         }
307
308         /*
309          * (non-Javadoc)
310          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
311          */

312         @Override
313         public Object convert(Object source) {
314
315             if (!(source instanceof Tuple)) {
316                 return source;
317             }
318
319             Tuple tuple = (Tuple) source;
320             List<TupleElement<?>> elements = tuple.getElements();
321
322             if (elements.size() == 1) {
323
324                 Object value = tuple.get(elements.get(0));
325
326                 if (type.isInstance(value) || value == null) {
327                     return value;
328                 }
329             }
330
331             return new TupleBackedMap(tuple);
332         }
333
334         /**
335          * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided
336          * {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the
337          * key/entry set.
338          *
339          * @author Jens Schauder
340          */

341         private static class TupleBackedMap implements Map<String, Object> {
342
343             private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified.";
344
345             private final Tuple tuple;
346
347             TupleBackedMap(Tuple tuple) {
348                 this.tuple = tuple;
349             }
350
351             @Override
352             public int size() {
353                 return tuple.getElements().size();
354             }
355
356             @Override
357             public boolean isEmpty() {
358                 return tuple.getElements().isEmpty();
359             }
360
361             /**
362              * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}.
363              * Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}.
364              *
365              * @param key the key for which to get the value from the map.
366              * @return whether the key is an element of the backing tuple.
367              */

368             @Override
369             public boolean containsKey(Object key) {
370
371                 try {
372                     tuple.get((String) key);
373                     return true;
374                 } catch (IllegalArgumentException e) {
375                     return false;
376                 }
377             }
378
379             @Override
380             public boolean containsValue(Object value) {
381                 return Arrays.asList(tuple.toArray()).contains(value);
382             }
383
384             /**
385              * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}.
386              * Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}.
387              *
388              * @param key the key for which to get the value from the map.
389              * @return the value of the backing {@link Tuple} for that key or {@code null}.
390              */

391             @Override
392             @Nullable
393             public Object get(Object key) {
394
395                 if (!(key instanceof String)) {
396                     return null;
397                 }
398
399                 try {
400                     return tuple.get((String) key);
401                 } catch (IllegalArgumentException e) {
402                     return null;
403                 }
404             }
405
406             @Override
407             public Object put(String key, Object value) {
408                 throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
409             }
410
411             @Override
412             public Object remove(Object key) {
413                 throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
414             }
415
416             @Override
417             public void putAll(Map<? extends String, ?> m) {
418                 throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
419             }
420
421             @Override
422             public void clear() {
423                 throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
424             }
425
426             @Override
427             public Set<String> keySet() {
428
429                 return tuple.getElements().stream() //
430                         .map(TupleElement::getAlias) //
431                         .collect(Collectors.toSet());
432             }
433
434             @Override
435             public Collection<Object> values() {
436                 return Arrays.asList(tuple.toArray());
437             }
438
439             @Override
440             public Set<Entry<String, Object>> entrySet() {
441
442                 return tuple.getElements().stream() //
443                         .map(e -> new HashMap.SimpleEntry<String, Object>(e.getAlias(), tuple.get(e))) //
444                         .collect(Collectors.toSet());
445             }
446         }
447     }
448 }
449