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.repository.core.support;
17
18 import static org.springframework.core.GenericTypeResolver.*;
19
20 import lombok.Value;
21
22 import java.lang.reflect.GenericDeclaration;
23 import java.lang.reflect.Method;
24 import java.lang.reflect.Type;
25 import java.lang.reflect.TypeVariable;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Optional;
31 import java.util.function.Predicate;
32 import java.util.function.Supplier;
33 import java.util.stream.IntStream;
34 import java.util.stream.Stream;
35
36 import org.springframework.core.MethodParameter;
37 import org.springframework.core.ResolvableType;
38 import org.springframework.data.repository.Repository;
39 import org.springframework.data.repository.core.RepositoryMetadata;
40 import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate;
41 import org.springframework.data.repository.util.QueryExecutionConverters;
42 import org.springframework.data.repository.util.ReactiveWrapperConverters;
43 import org.springframework.util.Assert;
44
45 /**
46  * Implementations of method lookup functions.
47  *
48  * @author Mark Paluch
49  * @author Oliver Gierke
50  * @since 2.0
51  */

52 interface MethodLookups {
53
54     /**
55      * Direct method lookup filtering on exact method name, parameter count and parameter types.
56      *
57      * @return direct method lookup.
58      */

59     public static MethodLookup direct() {
60
61         MethodPredicate direct = (invoked, candidate) -> candidate.getName().equals(invoked.getName())
62                 && candidate.getParameterCount() == invoked.getParameterCount()
63                 && Arrays.equals(candidate.getParameterTypes(), invoked.getParameterTypes());
64
65         return () -> Collections.singletonList(direct);
66     }
67
68     /**
69      * Repository type-aware method lookup composed of {@link #direct()} and {@link RepositoryAwareMethodLookup}.
70      * <p/>
71      * Repository-aware lookups resolve generic types from the repository declaration to verify assignability to Id/domain
72      * types. This lookup also permits assignable method signatures but prefers {@link #direct()} matches.
73      *
74      * @param repositoryMetadata must not be {@literal null}.
75      * @return the composed, repository-aware method lookup.
76      * @see #direct()
77      */

78     public static MethodLookup forRepositoryTypes(RepositoryMetadata repositoryMetadata) {
79         return direct().and(new RepositoryAwareMethodLookup(repositoryMetadata));
80     }
81
82     /**
83      * Repository type-aware method lookup composed of {@link #direct()} and {@link ReactiveTypeInteropMethodLookup}.
84      * <p/>
85      * This method lookup considers adaptability of reactive types in method signatures. Repository methods accepting a
86      * reactive type can be possibly called with a different reactive type if the reactive type can be adopted to the
87      * target type. This lookup also permits assignable method signatures and resolves repository id/entity types but
88      * prefers {@link #direct()} matches.
89      *
90      * @param repositoryMetadata must not be {@literal null}.
91      * @return the composed, repository-aware method lookup.
92      * @see #direct()
93      * @see #forRepositoryTypes(RepositoryMetadata)
94      */

95     public static MethodLookup forReactiveTypes(RepositoryMetadata repositoryMetadata) {
96         return direct().and(new ReactiveTypeInteropMethodLookup(repositoryMetadata));
97     }
98
99     /**
100      * Default {@link MethodLookup} considering repository Id and entity types permitting calls to methods with assignable
101      * arguments.
102      *
103      * @author Mark Paluch
104      */

105     static class RepositoryAwareMethodLookup implements MethodLookup {
106
107         @SuppressWarnings("rawtypes"private static final TypeVariable<Class<Repository>>[] PARAMETERS = Repository.class
108                 .getTypeParameters();
109         private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName();
110         private static final String ID_TYPE_NAME = PARAMETERS[1].getName();
111
112         private final ResolvableType entityType, idType;
113         private final Class<?> repositoryInterface;
114
115         /**
116          * Creates a new {@link RepositoryAwareMethodLookup} for the given {@link RepositoryMetadata}.
117          *
118          * @param repositoryMetadata must not be {@literal null}.
119          */

120         public RepositoryAwareMethodLookup(RepositoryMetadata repositoryMetadata) {
121
122             Assert.notNull(repositoryMetadata, "Repository metadata must not be null!");
123
124             this.entityType = ResolvableType.forClass(repositoryMetadata.getDomainType());
125             this.idType = ResolvableType.forClass(repositoryMetadata.getIdType());
126             this.repositoryInterface = repositoryMetadata.getRepositoryInterface();
127         }
128
129         /*
130          * (non-Javadoc)
131          * @see org.springframework.data.repository.core.support.MethodLookup#getLookups()
132          */

133         @Override
134         public List<MethodPredicate> getLookups() {
135
136             MethodPredicate detailedComparison = (invoked, candidate) -> Optional.of(candidate)
137                     .filter(baseClassMethod -> baseClassMethod.getName().equals(invoked.getName()))// Right name
138                     .filter(baseClassMethod -> baseClassMethod.getParameterCount() == invoked.getParameterCount())
139                     .filter(baseClassMethod -> parametersMatch(invoked.getMethod(), baseClassMethod))// All parameters match
140                     .isPresent();
141
142             return Collections.singletonList(detailedComparison);
143         }
144
145         /**
146          * Checks whether the given parameter type matches the generic type of the given parameter. Thus when {@literal PK}
147          * is declared, the method ensures that given method parameter is the primary key type declared in the given
148          * repository interface e.g.
149          *
150          * @param variable must not be {@literal null}.
151          * @param parameterType must not be {@literal null}.
152          * @return
153          */

154         protected boolean matchesGenericType(TypeVariable<?> variable, ResolvableType parameterType) {
155
156             GenericDeclaration declaration = variable.getGenericDeclaration();
157
158             if (declaration instanceof Class) {
159
160                 if (ID_TYPE_NAME.equals(variable.getName()) && parameterType.isAssignableFrom(idType)) {
161                     return true;
162                 }
163
164                 Type boundType = variable.getBounds()[0];
165                 String referenceName = boundType instanceof TypeVariable ? boundType.toString() : variable.toString();
166
167                 return DOMAIN_TYPE_NAME.equals(referenceName) && parameterType.isAssignableFrom(entityType);
168             }
169
170             for (Type type : variable.getBounds()) {
171                 if (ResolvableType.forType(type).isAssignableFrom(parameterType)) {
172                     return true;
173                 }
174             }
175
176             return false;
177         }
178
179         /**
180          * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
181          * against the ones bound in the given repository interface.
182          *
183          * @param invokedMethod
184          * @param candidate
185          * @return
186          */

187         private boolean parametersMatch(Method invokedMethod, Method candidate) {
188
189             Class<?>[] methodParameterTypes = invokedMethod.getParameterTypes();
190             Type[] genericTypes = candidate.getGenericParameterTypes();
191             Class<?>[] types = candidate.getParameterTypes();
192
193             for (int i = 0; i < genericTypes.length; i++) {
194
195                 Type genericType = genericTypes[i];
196                 Class<?> type = types[i];
197                 MethodParameter parameter = new MethodParameter(invokedMethod, i);
198                 Class<?> parameterType = resolveParameterType(parameter, repositoryInterface);
199
200                 if (genericType instanceof TypeVariable<?>) {
201
202                     if (!matchesGenericType((TypeVariable<?>) genericType, ResolvableType.forMethodParameter(parameter))) {
203                         return false;
204                     }
205
206                     continue;
207                 }
208
209                 if (types[i].equals(parameterType)) {
210                     continue;
211                 }
212
213                 if (!type.isAssignableFrom(parameterType) || !type.equals(methodParameterTypes[i])) {
214                     return false;
215                 }
216             }
217
218             return true;
219         }
220     }
221
222     /**
223      * Extension to {@link RepositoryAwareMethodLookup} considering reactive type adoption and entity types permitting
224      * calls to methods with assignable arguments.
225      *
226      * @author Mark Paluch
227      */

228     static class ReactiveTypeInteropMethodLookup extends RepositoryAwareMethodLookup {
229
230         private final RepositoryMetadata repositoryMetadata;
231
232         public ReactiveTypeInteropMethodLookup(RepositoryMetadata repositoryMetadata) {
233
234             super(repositoryMetadata);
235             this.repositoryMetadata = repositoryMetadata;
236         }
237
238         /*
239          * (non-Javadoc)
240          * @see org.springframework.data.repository.core.support.MethodLookups.RepositoryAwareMethodLookup#getLookups()
241          */

242         @Override
243         public List<MethodPredicate> getLookups() {
244
245             MethodPredicate convertibleComparison = (invokedMethod, candidate) -> {
246
247                 List<Supplier<Optional<Method>>> suppliers = new ArrayList<>();
248
249                 if (usesParametersWithReactiveWrappers(invokedMethod.getMethod())) {
250                     suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, assignableWrapperMatch())); //
251                     suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, wrapperConversionMatch()));
252                 }
253
254                 return suppliers.stream().anyMatch(supplier -> supplier.get().isPresent());
255             };
256
257             MethodPredicate detailedComparison = (invokedMethod, candidate) -> getMethodCandidate(invokedMethod, candidate,
258                     matchParameterOrComponentType(repositoryMetadata.getRepositoryInterface())).isPresent();
259
260             return Arrays.asList(convertibleComparison, detailedComparison);
261         }
262
263         /**
264          * {@link Predicate} to check parameter assignability between a parameters in which the declared parameter may be
265          * wrapped but supports unwrapping. Usually types like {@link Optional} or {@link java.util.stream.Stream}.
266          *
267          * @param repositoryInterface
268          * @return
269          * @see QueryExecutionConverters
270          * @see #matchesGenericType
271          */

272         private Predicate<ParameterOverrideCriteria> matchParameterOrComponentType(Class<?> repositoryInterface) {
273
274             return (parameterCriteria) -> {
275
276                 Class<?> parameterType = resolveParameterType(parameterCriteria.getDeclared(), repositoryInterface);
277                 Type genericType = parameterCriteria.getGenericBaseType();
278
279                 if (genericType instanceof TypeVariable<?>) {
280
281                     if (!matchesGenericType((TypeVariable<?>) genericType,
282                             ResolvableType.forMethodParameter(parameterCriteria.getDeclared()))) {
283                         return false;
284                     }
285                 }
286
287                 return parameterCriteria.getBaseType().isAssignableFrom(parameterType)
288                         && parameterCriteria.isAssignableFromDeclared();
289             };
290         }
291
292         /**
293          * Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped.
294          *
295          * @param parameterType must not be {@literal null}.
296          * @return
297          */

298         private static boolean isNonUnwrappingWrapper(Class<?> parameterType) {
299
300             Assert.notNull(parameterType, "Parameter type must not be null!");
301
302             return QueryExecutionConverters.supports(parameterType)
303                     && !QueryExecutionConverters.supportsUnwrapping(parameterType);
304         }
305
306         /**
307          * Returns whether the given {@link Method} uses a reactive wrapper type as parameter.
308          *
309          * @param method must not be {@literal null}.
310          * @return
311          */

312         private static boolean usesParametersWithReactiveWrappers(Method method) {
313
314             Assert.notNull(method, "Method must not be null!");
315
316             return Arrays.stream(method.getParameterTypes())//
317                     .anyMatch(ReactiveTypeInteropMethodLookup::isNonUnwrappingWrapper);
318         }
319
320         /**
321          * Returns a candidate method from the base class for the given one or the method given in the first place if none
322          * one the base class matches.
323          *
324          * @param method must not be {@literal null}.
325          * @param baseClass must not be {@literal null}.
326          * @param predicate must not be {@literal null}.
327          * @return
328          */

329         private static Optional<Method> getMethodCandidate(InvokedMethod invokedMethod, Method candidate,
330                 Predicate<ParameterOverrideCriteria> predicate) {
331
332             return Optional.of(candidate)//
333                     .filter(it -> invokedMethod.getName().equals(it.getName()))//
334                     .filter(it -> parameterCountMatch(invokedMethod, it))//
335                     .filter(it -> parametersMatch(invokedMethod.getMethod(), it, predicate));
336         }
337
338         /**
339          * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
340          * against the ones bound in the given repository interface.
341          *
342          * @param declaredMethod must not be {@literal null}.
343          * @param baseClassMethod must not be {@literal null}.
344          * @param predicate must not be {@literal null}.
345          * @return
346          */

347         private static boolean parametersMatch(Method declaredMethod, Method baseClassMethod,
348                 Predicate<ParameterOverrideCriteria> predicate) {
349             return methodParameters(declaredMethod, baseClassMethod).allMatch(predicate);
350         }
351
352         /**
353          * {@link Predicate} to check whether a method parameter is a {@link #isNonUnwrappingWrapper(Class)} and can be
354          * converted into a different wrapper. Usually {@link rx.Observable} to {@link org.reactivestreams.Publisher}
355          * conversion.
356          *
357          * @return
358          */

359         private static Predicate<ParameterOverrideCriteria> wrapperConversionMatch() {
360
361             return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
362                     && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
363                     && ReactiveWrapperConverters.canConvert(parameterCriteria.getDeclaredType(), parameterCriteria.getBaseType());
364         }
365
366         /**
367          * {@link Predicate} to check parameter assignability between a {@link #isNonUnwrappingWrapper(Class)} parameter and
368          * a declared parameter. Usually {@link reactor.core.publisher.Flux} vs. {@link org.reactivestreams.Publisher}
369          * conversion.
370          *
371          * @return
372          */

373         private static Predicate<ParameterOverrideCriteria> assignableWrapperMatch() {
374
375             return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) //
376                     && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) //
377                     && parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType());
378         }
379
380         private static boolean parameterCountMatch(InvokedMethod invokedMethod, Method baseClassMethod) {
381
382             ImplementationInvocationMetadata invocationMetadata = new ImplementationInvocationMetadata(
383                     invokedMethod.getMethod(), baseClassMethod);
384             return invocationMetadata.canInvoke(invokedMethod.getMethod(), baseClassMethod);
385         }
386
387         private static Stream<ParameterOverrideCriteria> methodParameters(Method invokedMethod, Method baseClassMethod) {
388             return IntStream.range(0, baseClassMethod.getParameterCount()) //
389                     .mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(invokedMethod, index),
390                             new MethodParameter(baseClassMethod, index)));
391         }
392
393         /**
394          * Criterion to represent {@link MethodParameter}s from a base method and its declared (overridden) method. Method
395          * parameters indexes are correlated so {@link ParameterOverrideCriteria} applies only to methods with same
396          * parameter count.
397          */

398         @Value(staticConstructor = "of")
399         static class ParameterOverrideCriteria {
400
401             private final MethodParameter declared;
402             private final MethodParameter base;
403
404             /**
405              * @return base method parameter type.
406              */

407             public Class<?> getBaseType() {
408                 return base.getParameterType();
409             }
410
411             /**
412              * @return generic base method parameter type.
413              */

414             public Type getGenericBaseType() {
415                 return base.getGenericParameterType();
416             }
417
418             /**
419              * @return declared method parameter type.
420              */

421             public Class<?> getDeclaredType() {
422                 return declared.getParameterType();
423             }
424
425             public boolean isAssignableFromDeclared() {
426                 return getBaseType().isAssignableFrom(getDeclaredType());
427             }
428         }
429     }
430 }
431