1 /*
2  * Copyright 2019-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 kotlin.coroutines.Continuation;
19 import kotlin.reflect.KFunction;
20 import kotlinx.coroutines.reactive.AwaitKt;
21
22 import java.lang.reflect.Method;
23
24 import org.reactivestreams.Publisher;
25
26 import org.springframework.core.KotlinDetector;
27 import org.springframework.data.repository.util.ReactiveWrapperConverters;
28 import org.springframework.data.util.KotlinReflectionUtils;
29 import org.springframework.data.util.ReflectionUtils;
30 import org.springframework.lang.Nullable;
31
32 /**
33  * Metadata for a implementation {@link Method} invocation. This value object encapsulates whether the called and the
34  * backing method are regular methods or suspendable Kotlin coroutines methods. It also allows invocation of suspended
35  * methods by backing the invocation using methods returning reactive types.
36  *
37  * @author Mark Paluch
38  * @since 2.3
39  */

40 class ImplementationInvocationMetadata {
41
42     private final boolean suspendedDeclaredMethod;
43     private final boolean suspendedBaseClassMethod;
44     private final boolean reactiveBaseClassMethod;
45
46     ImplementationInvocationMetadata(Method declaredMethod, Method baseClassMethod) {
47
48         if (!KotlinDetector.isKotlinReflectPresent()) {
49             suspendedDeclaredMethod = false;
50             suspendedBaseClassMethod = false;
51             reactiveBaseClassMethod = false;
52             return;
53         }
54
55         KFunction<?> declaredFunction = KotlinDetector.isKotlinType(declaredMethod.getDeclaringClass())
56                 ? KotlinReflectionUtils.findKotlinFunction(declaredMethod)
57                 : null;
58         KFunction<?> baseClassFunction = KotlinDetector.isKotlinType(baseClassMethod.getDeclaringClass())
59                 ? KotlinReflectionUtils.findKotlinFunction(baseClassMethod)
60                 : null;
61
62         suspendedDeclaredMethod = declaredFunction != null && declaredFunction.isSuspend();
63         suspendedBaseClassMethod = baseClassFunction != null && baseClassFunction.isSuspend();
64         this.reactiveBaseClassMethod = !suspendedBaseClassMethod
65                 && ReactiveWrapperConverters.supports(baseClassMethod.getReturnType());
66     }
67
68     @Nullable
69     public Object invoke(Method methodToCall, Object instance, Object[] args) throws Throwable {
70
71         return shouldAdaptReactiveToSuspended() ? invokeReactiveToSuspend(methodToCall, instance, args)
72                 : methodToCall.invoke(instance, args);
73
74     }
75
76     private boolean shouldAdaptReactiveToSuspended() {
77         return suspendedDeclaredMethod && !suspendedBaseClassMethod && reactiveBaseClassMethod;
78     }
79
80     @Nullable
81     @SuppressWarnings({ "unchecked""ConstantConditions" })
82     private Object invokeReactiveToSuspend(Method methodToCall, Object instance, Object[] args)
83             throws ReflectiveOperationException {
84
85         /*
86          * Kotlin suspended functions are invoked with a synthetic Continuation parameter that keeps track of the Coroutine context.
87          * We're invoking a method without Continuation as we expect the method to return any sort of reactive type,
88          * therefore we need to strip the Continuation parameter.
89          */

90         Object[] invocationArguments = new Object[args.length - 1];
91         System.arraycopy(args, 0, invocationArguments, 0, invocationArguments.length);
92         Object result = methodToCall.invoke(instance, invocationArguments);
93
94         Publisher<?> publisher = result instanceof Publisher ? (Publisher<?>) result
95                 : ReactiveWrapperConverters.toWrapper(result, Publisher.class);
96
97         return AwaitKt.awaitFirstOrNull(publisher, (Continuation) args[args.length - 1]);
98     }
99
100     boolean canInvoke(Method invokedMethod, Method backendMethod) {
101
102         if (suspendedDeclaredMethod == suspendedBaseClassMethod) {
103             return invokedMethod.getParameterCount() == backendMethod.getParameterCount();
104         }
105
106         if (suspendedDeclaredMethod && reactiveBaseClassMethod) {
107             return invokedMethod.getParameterCount() - 1 == backendMethod.getParameterCount();
108         }
109
110         return false;
111     }
112 }
113