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.support;
17
18 import static org.springframework.data.querydsl.QuerydslUtils.*;
19
20 import lombok.extern.slf4j.Slf4j;
21
22 import java.io.Serializable;
23 import java.lang.reflect.Method;
24 import java.util.Optional;
25 import java.util.stream.Stream;
26
27 import javax.persistence.EntityManager;
28 import javax.persistence.Tuple;
29
30 import org.springframework.beans.factory.BeanFactory;
31 import org.springframework.dao.InvalidDataAccessApiUsageException;
32 import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
33 import org.springframework.data.jpa.provider.PersistenceProvider;
34 import org.springframework.data.jpa.provider.QueryExtractor;
35 import org.springframework.data.jpa.repository.JpaRepository;
36 import org.springframework.data.jpa.repository.query.AbstractJpaQuery;
37 import org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory;
38 import org.springframework.data.jpa.repository.query.EscapeCharacter;
39 import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy;
40 import org.springframework.data.jpa.repository.query.JpaQueryMethod;
41 import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory;
42 import org.springframework.data.jpa.util.JpaMetamodel;
43 import org.springframework.data.projection.ProjectionFactory;
44 import org.springframework.data.querydsl.EntityPathResolver;
45 import org.springframework.data.querydsl.QuerydslPredicateExecutor;
46 import org.springframework.data.querydsl.SimpleEntityPathResolver;
47 import org.springframework.data.repository.core.RepositoryInformation;
48 import org.springframework.data.repository.core.RepositoryMetadata;
49 import org.springframework.data.repository.core.support.QueryCreationListener;
50 import org.springframework.data.repository.core.support.RepositoryComposition;
51 import org.springframework.data.repository.core.support.RepositoryFactorySupport;
52 import org.springframework.data.repository.core.support.RepositoryFragment;
53 import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor;
54 import org.springframework.data.repository.query.QueryLookupStrategy;
55 import org.springframework.data.repository.query.QueryLookupStrategy.Key;
56 import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
57 import org.springframework.data.repository.query.ReturnedType;
58 import org.springframework.lang.Nullable;
59 import org.springframework.util.Assert;
60 import org.springframework.util.ReflectionUtils;
61
62 /**
63  * JPA specific generic repository factory.
64  *
65  * @author Oliver Gierke
66  * @author Mark Paluch
67  * @author Christoph Strobl
68  * @author Jens Schauder
69  * @author Stefan Fussenegger
70  * @author RĂ©da Housni Alaoui
71  */

72 public class JpaRepositoryFactory extends RepositoryFactorySupport {
73
74     private final EntityManager entityManager;
75     private final QueryExtractor extractor;
76     private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor;
77
78     private EntityPathResolver entityPathResolver;
79     private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
80     private JpaQueryMethodFactory queryMethodFactory;
81
82     /**
83      * Creates a new {@link JpaRepositoryFactory}.
84      *
85      * @param entityManager must not be {@literal null}
86      */

87     public JpaRepositoryFactory(EntityManager entityManager) {
88
89         Assert.notNull(entityManager, "EntityManager must not be null!");
90
91         this.entityManager = entityManager;
92         this.extractor = PersistenceProvider.fromEntityManager(entityManager);
93         this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
94         this.entityPathResolver = SimpleEntityPathResolver.INSTANCE;
95         this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor);
96
97         addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
98         addRepositoryProxyPostProcessor((factory, repositoryInformation) -> {
99
100             if (hasMethodReturningStream(repositoryInformation.getRepositoryInterface())) {
101                 factory.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE);
102             }
103         });
104
105         if (extractor.equals(PersistenceProvider.ECLIPSELINK)) {
106             addQueryCreationListener(new EclipseLinkProjectionQueryCreationListener(entityManager));
107         }
108     }
109
110     /*
111      * (non-Javadoc)
112      * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#setBeanClassLoader(java.lang.ClassLoader)
113      */

114     @Override
115     public void setBeanClassLoader(ClassLoader classLoader) {
116
117         super.setBeanClassLoader(classLoader);
118         this.crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader);
119     }
120
121     /**
122      * Configures the {@link EntityPathResolver} to be used. Defaults to {@link SimpleEntityPathResolver#INSTANCE}.
123      *
124      * @param entityPathResolver must not be {@literal null}.
125      */

126     public void setEntityPathResolver(EntityPathResolver entityPathResolver) {
127
128         Assert.notNull(entityPathResolver, "EntityPathResolver must not be null!");
129
130         this.entityPathResolver = entityPathResolver;
131     }
132
133     /**
134      * Configures the escape character to be used for like-expressions created for derived queries.
135      *
136      * @param escapeCharacter a character used for escaping in certain like expressions.
137      */

138     public void setEscapeCharacter(EscapeCharacter escapeCharacter) {
139         this.escapeCharacter = escapeCharacter;
140     }
141
142     /**
143      * Configures the {@link JpaQueryMethodFactory} to be used. Defaults to
144      * {@link JpaQueryMethod.DefaultJpaQueryMethodFactory#INSTANCE}.
145      *
146      * @param queryMethodFactory must not be {@literal null}.
147      */

148     public void setQueryMethodFactory(JpaQueryMethodFactory queryMethodFactory) {
149
150         Assert.notNull(queryMethodFactory, "QueryMethodFactory must not be null!");
151
152         this.queryMethodFactory = queryMethodFactory;
153     }
154
155     /*
156      * (non-Javadoc)
157      * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryMetadata)
158      */

159     @Override
160     protected final JpaRepositoryImplementation<?, ?> getTargetRepository(RepositoryInformation information) {
161
162         JpaRepositoryImplementation<?, ?> repository = getTargetRepository(information, entityManager);
163         repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata());
164         repository.setEscapeCharacter(escapeCharacter);
165
166         return repository;
167     }
168
169     /**
170      * Callback to create a {@link JpaRepository} instance with the given {@link EntityManager}
171      *
172      * @param information will never be {@literal null}.
173      * @param entityManager will never be {@literal null}.
174      * @return
175      */

176     protected JpaRepositoryImplementation<?, ?> getTargetRepository(RepositoryInformation information,
177             EntityManager entityManager) {
178
179         JpaEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType());
180         Object repository = getTargetRepositoryViaReflection(information, entityInformation, entityManager);
181
182         Assert.isInstanceOf(JpaRepositoryImplementation.class, repository);
183
184         return (JpaRepositoryImplementation<?, ?>) repository;
185     }
186
187     /*
188      * (non-Javadoc)
189      * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)
190      */

191     @Override
192     protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
193         return SimpleJpaRepository.class;
194     }
195
196     /*
197      * (non-Javadoc)
198      * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getProjectionFactory(java.lang.ClassLoader, org.springframework.beans.factory.BeanFactory)
199      */

200     @Override
201     protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) {
202
203         CollectionAwareProjectionFactory factory = new CollectionAwareProjectionFactory();
204         factory.setBeanClassLoader(classLoader);
205         factory.setBeanFactory(beanFactory);
206
207         return factory;
208     }
209
210     /*
211      * (non-Javadoc)
212      * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider)
213      */

214     @Override
215     protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
216             QueryMethodEvaluationContextProvider evaluationContextProvider) {
217         return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, evaluationContextProvider,
218                 escapeCharacter));
219     }
220
221     /*
222      * (non-Javadoc)
223      * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class)
224      */

225     @Override
226     @SuppressWarnings("unchecked")
227     public <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
228
229         return (JpaEntityInformation<T, ID>) JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
230     }
231
232     /*
233      * (non-Javadoc)
234      * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryFragments(org.springframework.data.repository.core.RepositoryMetadata)
235      */

236     @Override
237     protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
238
239         RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty();
240
241         boolean isQueryDslRepository = QUERY_DSL_PRESENT
242                 && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface());
243
244         if (isQueryDslRepository) {
245
246             if (metadata.isReactiveRepository()) {
247                 throw new InvalidDataAccessApiUsageException(
248                         "Cannot combine Querydsl and reactive repository support in a single interface");
249             }
250
251             JpaEntityInformation<?, Serializable> entityInformation = getEntityInformation(metadata.getDomainType());
252
253             Object querydslFragment = getTargetRepositoryViaReflection(QuerydslJpaPredicateExecutor.class, entityInformation,
254                     entityManager, entityPathResolver, crudMethodMetadataPostProcessor.getCrudMethodMetadata());
255
256             fragments = fragments.append(RepositoryFragment.implemented(querydslFragment));
257         }
258
259         return fragments;
260     }
261
262     private static boolean hasMethodReturningStream(Class<?> repositoryClass) {
263
264         Method[] methods = ReflectionUtils.getAllDeclaredMethods(repositoryClass);
265
266         for (Method method : methods) {
267             if (Stream.class.isAssignableFrom(method.getReturnType())) {
268                 return true;
269             }
270         }
271
272         return false;
273     }
274
275     /**
276      * Query creation listener that informs EclipseLink users that they have to be extra careful when defining repository
277      * query methods using projections as we have to rely on the declaration order of the accessors in projection
278      * interfaces matching the order in columns. Alias-based mapping doesn't work with EclipseLink as it doesn't support
279      * {@link Tuple} based queries yet.
280      *
281      * @author Oliver Gierke
282      * @since 2.0.5
283      * @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141
284      */

285     @Slf4j
286     private static class EclipseLinkProjectionQueryCreationListener implements QueryCreationListener<AbstractJpaQuery> {
287
288         private static final String ECLIPSELINK_PROJECTIONS = "Usage of Spring Data projections detected on persistence provider EclipseLink. Make sure the following query methods declare result columns in exactly the order the accessors are declared in the projecting interface or the order of parameters for DTOs:";
289
290         private final JpaMetamodel metamodel;
291
292         private boolean warningLogged = false;
293
294         /**
295          * Creates a new {@link EclipseLinkProjectionQueryCreationListener} for the given {@link EntityManager}.
296          *
297          * @param em must not be {@literal null}.
298          */

299         public EclipseLinkProjectionQueryCreationListener(EntityManager em) {
300
301             Assert.notNull(em, "EntityManager must not be null!");
302
303             this.metamodel = JpaMetamodel.of(em.getMetamodel());
304         }
305
306         /*
307          * (non-Javadoc)
308          * @see org.springframework.data.repository.core.support.QueryCreationListener#onCreation(org.springframework.data.repository.query.RepositoryQuery)
309          */

310         @Override
311         public void onCreation(AbstractJpaQuery query) {
312
313             JpaQueryMethod queryMethod = query.getQueryMethod();
314             ReturnedType type = queryMethod.getResultProcessor().getReturnedType();
315
316             if (type.isProjecting() && !metamodel.isJpaManaged(type.getReturnedType())) {
317
318                 if (!warningLogged) {
319                     log.info(ECLIPSELINK_PROJECTIONS);
320                     this.warningLogged = true;
321                 }
322
323                 log.info(" - {}", queryMethod);
324             }
325         }
326     }
327 }
328