Skip to content

Commit 2cab940

Browse files
committed
DATACMNS-836 - Support reactive wrapper type conversion.
We now support reactive wrapper type conversion without unwrapping. Project Reactor types can be converted to RxJava types and vice versa. This allows repositories to be composed of Project Reactor and RxJava query methods. Methods can call implementation methods using parameter conversion. Method parameters and response types can be converted and allow mixed usage of different wrapper types. The method selection uses a multi-pass candidate selection to invoke the most appropriate method (exact arguments, convertible wrappers, assignable arguments). interface RxJavaRepository extends Repository<Person, String> { Single<Boolean> exists(Serializable id); Single<Boolean> exists(Single<Serializable> id); Single<Boolean> exists(Mono<Serializable> id); } implementation: ReactiveCrudRepository<Person, String> { Mono<Boolean> exists(Serializable id); Mono<Boolean> exists(Mono<id>); }
1 parent d317b8a commit 2cab940

17 files changed

+1637
-60
lines changed

src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ public Set<Class<?>> getAlternativeDomainTypes() {
334334

335335
/**
336336
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
337-
* agains the ones bound in the given repository interface.
337+
* against the ones bound in the given repository interface.
338338
*
339339
* @param method
340340
* @param baseClassMethod
@@ -379,7 +379,7 @@ private boolean parametersMatch(Method method, Method baseClassMethod) {
379379
* @param parameterType
380380
* @return
381381
*/
382-
private boolean matchesGenericType(TypeVariable<?> variable, Class<?> parameterType) {
382+
boolean matchesGenericType(TypeVariable<?> variable, Class<?> parameterType) {
383383

384384
Class<?> entityType = getDomainType();
385385
Class<?> idClass = getIdType();

src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* Simple domain service to convert query results into a dedicated type.
2626
*
2727
* @author Oliver Gierke
28+
* @author Mark Paluch
2829
*/
2930
class QueryExecutionResultHandler {
3031

@@ -47,25 +48,36 @@ public QueryExecutionResultHandler() {
4748
* Post-processes the given result of a query invocation to the given type.
4849
*
4950
* @param result can be {@literal null}.
50-
* @param returnTypeDesciptor can be {@literal null}, if so, no conversion is performed.
51+
* @param returnTypeDescriptor can be {@literal null}, if so, no conversion is performed.
5152
* @return
5253
*/
53-
public Object postProcessInvocationResult(Object result, TypeDescriptor returnTypeDesciptor) {
54+
public Object postProcessInvocationResult(Object result, TypeDescriptor returnTypeDescriptor) {
5455

55-
if (returnTypeDesciptor == null) {
56+
if (returnTypeDescriptor == null) {
5657
return result;
5758
}
5859

59-
Class<?> expectedReturnType = returnTypeDesciptor.getType();
60+
Class<?> expectedReturnType = returnTypeDescriptor.getType();
6061

6162
if (result != null && expectedReturnType.isInstance(result)) {
6263
return result;
6364
}
6465

65-
if (QueryExecutionConverters.supports(expectedReturnType)
66-
&& conversionService.canConvert(WRAPPER_TYPE, returnTypeDesciptor)
67-
&& !conversionService.canBypassConvert(WRAPPER_TYPE, TypeDescriptor.valueOf(expectedReturnType))) {
68-
return conversionService.convert(new NullableWrapper(result), expectedReturnType);
66+
if (QueryExecutionConverters.supports(expectedReturnType)) {
67+
68+
TypeDescriptor targetType = TypeDescriptor.valueOf(expectedReturnType);
69+
70+
if(conversionService.canConvert(WRAPPER_TYPE, returnTypeDescriptor)
71+
&& !conversionService.canBypassConvert(WRAPPER_TYPE, targetType)) {
72+
73+
return conversionService.convert(new NullableWrapper(result), expectedReturnType);
74+
}
75+
76+
if(result != null && conversionService.canConvert(TypeDescriptor.valueOf(result.getClass()), returnTypeDescriptor)
77+
&& !conversionService.canBypassConvert(TypeDescriptor.valueOf(result.getClass()), targetType)) {
78+
79+
return conversionService.convert(result, expectedReturnType);
80+
}
6981
}
7082

7183
if (result == null) {
@@ -75,4 +87,5 @@ public Object postProcessInvocationResult(Object result, TypeDescriptor returnTy
7587
return conversionService.canConvert(result.getClass(), expectedReturnType) ? conversionService.convert(result,
7688
expectedReturnType) : result;
7789
}
90+
7891
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/*
2+
* Copyright 2016 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+
* http://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 java.lang.reflect.Method;
21+
import java.lang.reflect.Type;
22+
import java.lang.reflect.TypeVariable;
23+
import java.util.function.BiPredicate;
24+
25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.core.convert.ConversionService;
27+
import org.springframework.data.repository.core.RepositoryInformation;
28+
import org.springframework.data.repository.core.RepositoryMetadata;
29+
import org.springframework.data.repository.util.QueryExecutionConverters;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* This {@link RepositoryInformation} uses a {@link ConversionService} to check whether method arguments can be
34+
* converted for invocation of implementation methods.
35+
*
36+
* @author Mark Paluch
37+
*/
38+
public class ReactiveRepositoryInformation extends DefaultRepositoryInformation {
39+
40+
private final ConversionService conversionService;
41+
42+
/**
43+
* Creates a new {@link ReactiveRepositoryInformation} for the given repository interface and repository base class
44+
* using a {@link ConversionService}.
45+
*
46+
* @param metadata must not be {@literal null}.
47+
* @param repositoryBaseClass must not be {@literal null}.
48+
* @param customImplementationClass
49+
* @param conversionService must not be {@literal null}.
50+
*/
51+
public ReactiveRepositoryInformation(RepositoryMetadata metadata, Class<?> repositoryBaseClass,
52+
Class<?> customImplementationClass, ConversionService conversionService) {
53+
54+
super(metadata, repositoryBaseClass, customImplementationClass);
55+
56+
Assert.notNull(conversionService, "Conversion service must not be null!");
57+
58+
this.conversionService = conversionService;
59+
}
60+
61+
/**
62+
* Returns the given target class' method if the given method (declared in the repository interface) was also declared
63+
* at the target class. Returns the given method if the given base class does not declare the method given. Takes
64+
* generics into account.
65+
*
66+
* @param method must not be {@literal null}
67+
* @param baseClass
68+
* @return
69+
*/
70+
Method getTargetClassMethod(Method method, Class<?> baseClass) {
71+
72+
if (baseClass == null) {
73+
return method;
74+
}
75+
76+
boolean wantsWrappers = wantsMethodUsingReactiveWrapperParameters(method);
77+
78+
if (wantsWrappers) {
79+
Method candidate = getMethodCandidate(method, baseClass, new ExactWrapperMatch(method));
80+
81+
if (candidate != null) {
82+
return candidate;
83+
}
84+
85+
candidate = getMethodCandidate(method, baseClass, new WrapperConversionMatch(method, conversionService));
86+
87+
if (candidate != null) {
88+
return candidate;
89+
}
90+
}
91+
92+
Method candidate = getMethodCandidate(method, baseClass,
93+
new MatchParameterOrComponentType(method, getRepositoryInterface()));
94+
95+
if (candidate != null) {
96+
return candidate;
97+
}
98+
99+
return method;
100+
}
101+
102+
private boolean wantsMethodUsingReactiveWrapperParameters(Method method) {
103+
104+
boolean wantsWrappers = false;
105+
106+
for (Class<?> parameterType : method.getParameterTypes()) {
107+
if (isNonunwrappingWrapper(parameterType)) {
108+
wantsWrappers = true;
109+
break;
110+
}
111+
}
112+
113+
return wantsWrappers;
114+
}
115+
116+
private Method getMethodCandidate(Method method, Class<?> baseClass, BiPredicate<Class<?>, Integer> predicate) {
117+
118+
for (Method baseClassMethod : baseClass.getMethods()) {
119+
120+
// Wrong name
121+
if (!method.getName().equals(baseClassMethod.getName())) {
122+
continue;
123+
}
124+
125+
// Wrong number of arguments
126+
if (!(method.getParameterTypes().length == baseClassMethod.getParameterTypes().length)) {
127+
continue;
128+
}
129+
130+
// Check whether all parameters match
131+
if (!parametersMatch(method, baseClassMethod, predicate)) {
132+
continue;
133+
}
134+
135+
return baseClassMethod;
136+
}
137+
138+
return null;
139+
}
140+
141+
/**
142+
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
143+
* against the ones bound in the given repository interface.
144+
*
145+
* @param method
146+
* @param baseClassMethod
147+
* @param predicate
148+
* @return
149+
*/
150+
private boolean parametersMatch(Method method, Method baseClassMethod, BiPredicate<Class<?>, Integer> predicate) {
151+
152+
Type[] genericTypes = baseClassMethod.getGenericParameterTypes();
153+
Class<?>[] types = baseClassMethod.getParameterTypes();
154+
for (int i = 0; i < genericTypes.length; i++) {
155+
156+
Type genericType = genericTypes[i];
157+
Class<?> type = types[i];
158+
MethodParameter parameter = new MethodParameter(method, i);
159+
Class<?> parameterType = resolveParameterType(parameter, getRepositoryInterface());
160+
161+
if (genericType instanceof TypeVariable<?>) {
162+
163+
if (!matchesGenericType((TypeVariable<?>) genericType, parameterType)) {
164+
return false;
165+
}
166+
continue;
167+
}
168+
169+
if (!predicate.test(type, i)) {
170+
return false;
171+
}
172+
}
173+
174+
return true;
175+
}
176+
177+
/**
178+
* Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped.
179+
*
180+
* @param parameterType
181+
* @return
182+
*/
183+
static boolean isNonunwrappingWrapper(Class<?> parameterType) {
184+
return QueryExecutionConverters.supports(parameterType)
185+
&& !QueryExecutionConverters.supportsUnwrapping(parameterType);
186+
}
187+
188+
static class WrapperConversionMatch implements BiPredicate<Class<?>, Integer> {
189+
190+
final Method declaredMethod;
191+
final Class<?>[] declaredParameterTypes;
192+
final ConversionService conversionService;
193+
194+
public WrapperConversionMatch(Method declaredMethod, ConversionService conversionService) {
195+
196+
this.declaredMethod = declaredMethod;
197+
this.declaredParameterTypes = declaredMethod.getParameterTypes();
198+
this.conversionService = conversionService;
199+
}
200+
201+
@Override
202+
public boolean test(Class<?> candidateParameterType, Integer index) {
203+
204+
// TODO: should check for component type
205+
if (isNonunwrappingWrapper(candidateParameterType) && isNonunwrappingWrapper(declaredParameterTypes[index])) {
206+
207+
if (conversionService.canConvert(declaredParameterTypes[index], candidateParameterType)) {
208+
return true;
209+
}
210+
}
211+
212+
return false;
213+
}
214+
215+
}
216+
217+
static class ExactWrapperMatch implements BiPredicate<Class<?>, Integer> {
218+
219+
final Method declaredMethod;
220+
final Class<?>[] declaredParameterTypes;
221+
222+
public ExactWrapperMatch(Method declaredMethod) {
223+
224+
this.declaredMethod = declaredMethod;
225+
this.declaredParameterTypes = declaredMethod.getParameterTypes();
226+
}
227+
228+
@Override
229+
public boolean test(Class<?> candidateParameterType, Integer index) {
230+
231+
// TODO: should check for component type
232+
if (isNonunwrappingWrapper(candidateParameterType) && isNonunwrappingWrapper(declaredParameterTypes[index])) {
233+
234+
if (declaredParameterTypes[index].isAssignableFrom(candidateParameterType)) {
235+
return true;
236+
}
237+
}
238+
239+
return false;
240+
}
241+
242+
}
243+
244+
static class MatchParameterOrComponentType implements BiPredicate<Class<?>, Integer> {
245+
246+
final Method declaredMethod;
247+
final Class<?>[] declaredParameterTypes;
248+
final Class<?> repositoryInterface;
249+
250+
public MatchParameterOrComponentType(Method declaredMethod, Class<?> repositoryInterface) {
251+
252+
this.declaredMethod = declaredMethod;
253+
this.declaredParameterTypes = declaredMethod.getParameterTypes();
254+
this.repositoryInterface = repositoryInterface;
255+
}
256+
257+
@Override
258+
public boolean test(Class<?> candidateParameterType, Integer index) {
259+
260+
MethodParameter parameter = new MethodParameter(declaredMethod, index);
261+
Class<?> parameterType = resolveParameterType(parameter, repositoryInterface);
262+
263+
if (!candidateParameterType.isAssignableFrom(parameterType)
264+
|| !candidateParameterType.equals(declaredParameterTypes[index])) {
265+
return false;
266+
}
267+
268+
return true;
269+
}
270+
271+
}
272+
273+
}

0 commit comments

Comments
 (0)