diff --git a/pom.xml b/pom.xml
index cbe1a271a..5532ee074 100644
--- a/pom.xml
+++ b/pom.xml
@@ -105,6 +105,15 @@
true
+
+
+
+ io.reactivex
+ rxjava
+ ${rxjava}
+ true
+
+
diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java
index 93bb39914..71f9ce485 100644
--- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java
+++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java
@@ -336,7 +336,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
/**
* Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
- * agains the ones bound in the given repository interface.
+ * against the ones bound in the given repository interface.
*
* @param method
* @param baseClassMethod
@@ -381,7 +381,7 @@ class DefaultRepositoryInformation implements RepositoryInformation {
* @param parameterType must not be {@literal null}.
* @return
*/
- private boolean matchesGenericType(TypeVariable> variable, ResolvableType parameterType) {
+ protected boolean matchesGenericType(TypeVariable> variable, ResolvableType parameterType) {
GenericDeclaration declaration = variable.getGenericDeclaration();
diff --git a/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java b/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java
index 38acd529f..f585c3386 100644
--- a/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java
+++ b/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java
@@ -28,6 +28,7 @@ import org.springframework.data.repository.util.QueryExecutionConverters;
* Simple domain service to convert query results into a dedicated type.
*
* @author Oliver Gierke
+ * @author Mark Paluch
*/
class QueryExecutionResultHandler {
@@ -50,25 +51,36 @@ class QueryExecutionResultHandler {
* Post-processes the given result of a query invocation to the given type.
*
* @param result can be {@literal null}.
- * @param returnTypeDesciptor can be {@literal null}, if so, no conversion is performed.
+ * @param returnTypeDescriptor can be {@literal null}, if so, no conversion is performed.
* @return
*/
- public Object postProcessInvocationResult(Object result, TypeDescriptor returnTypeDesciptor) {
+ public Object postProcessInvocationResult(Object result, TypeDescriptor returnTypeDescriptor) {
- if (returnTypeDesciptor == null) {
+ if (returnTypeDescriptor == null) {
return result;
}
- Class> expectedReturnType = returnTypeDesciptor.getType();
+ Class> expectedReturnType = returnTypeDescriptor.getType();
if (result != null && expectedReturnType.isInstance(result)) {
return result;
}
- if (QueryExecutionConverters.supports(expectedReturnType)
- && conversionService.canConvert(WRAPPER_TYPE, returnTypeDesciptor)
- && !conversionService.canBypassConvert(WRAPPER_TYPE, TypeDescriptor.valueOf(expectedReturnType))) {
- return conversionService.convert(new NullableWrapper(result), expectedReturnType);
+ if (QueryExecutionConverters.supports(expectedReturnType)) {
+
+ TypeDescriptor targetType = TypeDescriptor.valueOf(expectedReturnType);
+
+ if(conversionService.canConvert(WRAPPER_TYPE, returnTypeDescriptor)
+ && !conversionService.canBypassConvert(WRAPPER_TYPE, targetType)) {
+
+ return conversionService.convert(new NullableWrapper(result), expectedReturnType);
+ }
+
+ if(result != null && conversionService.canConvert(TypeDescriptor.valueOf(result.getClass()), returnTypeDescriptor)
+ && !conversionService.canBypassConvert(TypeDescriptor.valueOf(result.getClass()), targetType)) {
+
+ return conversionService.convert(result, expectedReturnType);
+ }
}
if (result != null) {
@@ -82,4 +94,5 @@ class QueryExecutionResultHandler {
return null;
}
+
}
diff --git a/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformation.java
new file mode 100644
index 000000000..3d3eac6b6
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryInformation.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.core.support;
+
+import static org.springframework.core.GenericTypeResolver.*;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.function.BiPredicate;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.util.QueryExecutionConverters;
+import org.springframework.util.Assert;
+
+/**
+ * This {@link RepositoryInformation} uses a {@link ConversionService} to check whether method arguments can be
+ * converted for invocation of implementation methods.
+ *
+ * @author Mark Paluch
+ */
+public class ReactiveRepositoryInformation extends DefaultRepositoryInformation {
+
+ private final ConversionService conversionService;
+
+ /**
+ * Creates a new {@link ReactiveRepositoryInformation} for the given repository interface and repository base class
+ * using a {@link ConversionService}.
+ *
+ * @param metadata must not be {@literal null}.
+ * @param repositoryBaseClass must not be {@literal null}.
+ * @param customImplementationClass
+ * @param conversionService must not be {@literal null}.
+ */
+ public ReactiveRepositoryInformation(RepositoryMetadata metadata, Class> repositoryBaseClass,
+ Class> customImplementationClass, ConversionService conversionService) {
+
+ super(metadata, repositoryBaseClass, customImplementationClass);
+
+ Assert.notNull(conversionService, "Conversion service must not be null!");
+
+ this.conversionService = conversionService;
+ }
+
+ /**
+ * Returns the given target class' method if the given method (declared in the repository interface) was also declared
+ * at the target class. Returns the given method if the given base class does not declare the method given. Takes
+ * generics into account.
+ *
+ * @param method must not be {@literal null}
+ * @param baseClass
+ * @return
+ */
+ Method getTargetClassMethod(Method method, Class> baseClass) {
+
+ if (baseClass == null) {
+ return method;
+ }
+
+ boolean wantsWrappers = wantsMethodUsingReactiveWrapperParameters(method);
+
+ if (wantsWrappers) {
+ Method candidate = getMethodCandidate(method, baseClass, new ExactWrapperMatch(method));
+
+ if (candidate != null) {
+ return candidate;
+ }
+
+ candidate = getMethodCandidate(method, baseClass, new WrapperConversionMatch(method, conversionService));
+
+ if (candidate != null) {
+ return candidate;
+ }
+ }
+
+ Method candidate = getMethodCandidate(method, baseClass,
+ new MatchParameterOrComponentType(method, getRepositoryInterface()));
+
+ if (candidate != null) {
+ return candidate;
+ }
+
+ return method;
+ }
+
+ private boolean wantsMethodUsingReactiveWrapperParameters(Method method) {
+
+ boolean wantsWrappers = false;
+
+ for (Class> parameterType : method.getParameterTypes()) {
+ if (isNonunwrappingWrapper(parameterType)) {
+ wantsWrappers = true;
+ break;
+ }
+ }
+
+ return wantsWrappers;
+ }
+
+ private Method getMethodCandidate(Method method, Class> baseClass, BiPredicate, Integer> predicate) {
+
+ for (Method baseClassMethod : baseClass.getMethods()) {
+
+ // Wrong name
+ if (!method.getName().equals(baseClassMethod.getName())) {
+ continue;
+ }
+
+ // Wrong number of arguments
+ if (!(method.getParameterTypes().length == baseClassMethod.getParameterTypes().length)) {
+ continue;
+ }
+
+ // Check whether all parameters match
+ if (!parametersMatch(method, baseClassMethod, predicate)) {
+ continue;
+ }
+
+ return baseClassMethod;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments
+ * against the ones bound in the given repository interface.
+ *
+ * @param method
+ * @param baseClassMethod
+ * @param predicate
+ * @return
+ */
+ private boolean parametersMatch(Method method, Method baseClassMethod, BiPredicate, Integer> predicate) {
+
+ Type[] genericTypes = baseClassMethod.getGenericParameterTypes();
+ Class>[] types = baseClassMethod.getParameterTypes();
+
+ for (int i = 0; i < genericTypes.length; i++) {
+ if (!predicate.test(types[i], i)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped.
+ *
+ * @param parameterType
+ * @return
+ */
+ static boolean isNonunwrappingWrapper(Class> parameterType) {
+ return QueryExecutionConverters.supports(parameterType)
+ && !QueryExecutionConverters.supportsUnwrapping(parameterType);
+ }
+
+ static class WrapperConversionMatch implements BiPredicate, Integer> {
+
+ final Method declaredMethod;
+ final Class>[] declaredParameterTypes;
+ final ConversionService conversionService;
+
+ public WrapperConversionMatch(Method declaredMethod, ConversionService conversionService) {
+
+ this.declaredMethod = declaredMethod;
+ this.declaredParameterTypes = declaredMethod.getParameterTypes();
+ this.conversionService = conversionService;
+ }
+
+ @Override
+ public boolean test(Class> candidateParameterType, Integer index) {
+
+ // TODO: should check for component type
+ if (isNonunwrappingWrapper(candidateParameterType) && isNonunwrappingWrapper(declaredParameterTypes[index])) {
+
+ if (conversionService.canConvert(declaredParameterTypes[index], candidateParameterType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ }
+
+ static class ExactWrapperMatch implements BiPredicate, Integer> {
+
+ final Method declaredMethod;
+ final Class>[] declaredParameterTypes;
+
+ public ExactWrapperMatch(Method declaredMethod) {
+
+ this.declaredMethod = declaredMethod;
+ this.declaredParameterTypes = declaredMethod.getParameterTypes();
+ }
+
+ @Override
+ public boolean test(Class> candidateParameterType, Integer index) {
+
+ // TODO: should check for component type
+ if (isNonunwrappingWrapper(candidateParameterType) && isNonunwrappingWrapper(declaredParameterTypes[index])) {
+
+ if (declaredParameterTypes[index].isAssignableFrom(candidateParameterType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ }
+
+ static class MatchParameterOrComponentType implements BiPredicate, Integer> {
+
+ final Method declaredMethod;
+ final Class>[] declaredParameterTypes;
+ final Class> repositoryInterface;
+
+ public MatchParameterOrComponentType(Method declaredMethod, Class> repositoryInterface) {
+
+ this.declaredMethod = declaredMethod;
+ this.declaredParameterTypes = declaredMethod.getParameterTypes();
+ this.repositoryInterface = repositoryInterface;
+ }
+
+ @Override
+ public boolean test(Class> candidateParameterType, Integer index) {
+
+ MethodParameter parameter = new MethodParameter(declaredMethod, index);
+ Class> parameterType = resolveParameterType(parameter, repositoryInterface);
+
+ if (!candidateParameterType.isAssignableFrom(parameterType)
+ || !candidateParameterType.equals(declaredParameterTypes[index])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
index b45ec1757..4f0366e53 100644
--- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2015 the original author or authors.
+ * Copyright 2008-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
+import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -60,6 +61,7 @@ import org.springframework.util.ObjectUtils;
* detection strategy can be configured by setting {@link QueryLookupStrategy.Key}.
*
* @author Oliver Gierke
+ * @author Mark Paluch
*/
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware {
@@ -77,6 +79,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
private ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE;
private BeanFactory beanFactory;
+ private ConversionService conversionService;
private QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener();
@@ -102,6 +105,16 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
this.namedQueries = namedQueries == null ? PropertiesBasedNamedQueries.EMPTY : namedQueries;
}
+ /**
+ * Configures a {@link ConversionService} instance to convert method parameters when calling implementation methods on
+ * the base class or a custom implementation.
+ *
+ * @param conversionService the conversionService to set.
+ */
+ public void setConversionService(ConversionService conversionService) {
+ this.conversionService = conversionService;
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
@@ -217,7 +230,14 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
}
- result.addAdvice(new QueryExecutorMethodInterceptor(information, customImplementation, target));
+ result.addAdvice(new QueryExecutorMethodInterceptor(information));
+
+ if (conversionService == null) {
+ result.addAdvice(new ImplementationMethodExecutionInterceptor(information, customImplementation, target));
+ } else {
+ result.addAdvice(new ConvertingImplementationMethodExecutionInterceptor(information, customImplementation, target,
+ conversionService));
+ }
return (T) result.getProxy(classLoader);
}
@@ -252,7 +272,17 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
Class> repositoryBaseClass = this.repositoryBaseClass == null ? getRepositoryBaseClass(metadata)
: this.repositoryBaseClass;
- repositoryInformation = new DefaultRepositoryInformation(metadata, repositoryBaseClass, customImplementationClass);
+ if (conversionService == null) {
+ repositoryInformation = new DefaultRepositoryInformation(metadata, repositoryBaseClass,
+ customImplementationClass);
+ } else {
+ // TODO: Not sure this is the best idea but at some point we need to distinguish between
+ // methods that want to get unwrapped data from wrapped parameters and those which are
+ // just fine with wrappers because at some point a converting interceptor kicks in
+ repositoryInformation = new ReactiveRepositoryInformation(metadata, repositoryBaseClass,
+ customImplementationClass, conversionService);
+ }
+
repositoryInformationCache.put(cacheKey, repositoryInformation);
return repositoryInformation;
}
@@ -390,25 +420,15 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
private final Map queries = new ConcurrentHashMap();
- private final Object customImplementation;
- private final RepositoryInformation repositoryInformation;
private final QueryExecutionResultHandler resultHandler;
- private final Object target;
/**
* Creates a new {@link QueryExecutorMethodInterceptor}. Builds a model of {@link QueryMethod}s to be invoked on
* execution of repository interface methods.
*/
- public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, Object customImplementation,
- Object target) {
-
- Assert.notNull(repositoryInformation, "RepositoryInformation must not be null!");
- Assert.notNull(target, "Target must not be null!");
+ public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation) {
this.resultHandler = new QueryExecutionResultHandler();
- this.repositoryInformation = repositoryInformation;
- this.customImplementation = customImplementation;
- this.target = target;
QueryLookupStrategy lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
RepositoryFactorySupport.this.evaluationContextProvider);
@@ -472,16 +492,66 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
+ if (hasQueryFor(method)) {
+ return queries.get(method).execute(arguments);
+ }
+
+ return invocation.proceed();
+ }
+
+ /**
+ * Returns whether we know of a query to execute for the given {@link Method};
+ *
+ * @param method
+ * @return
+ */
+ private boolean hasQueryFor(Method method) {
+ return queries.containsKey(method);
+ }
+ }
+
+ /**
+ * Method interceptor that calls methods on either the base implementation or the custom repository implementation.
+ *
+ * @author Mark Paluch
+ */
+ public class ImplementationMethodExecutionInterceptor implements MethodInterceptor {
+
+ private final Object customImplementation;
+ private final RepositoryInformation repositoryInformation;
+ private final Object target;
+
+ /**
+ * Creates a new {@link QueryExecutorMethodInterceptor}. Builds a model of {@link QueryMethod}s to be invoked on
+ * execution of repository interface methods.
+ */
+ public ImplementationMethodExecutionInterceptor(RepositoryInformation repositoryInformation,
+ Object customImplementation, Object target) {
+
+ Assert.notNull(repositoryInformation, "RepositoryInformation must not be null!");
+ Assert.notNull(target, "Target must not be null!");
+
+ this.repositoryInformation = repositoryInformation;
+ this.customImplementation = customImplementation;
+ this.target = target;
+
+ }
+
+ /* (non-Javadoc)
+ * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
+ */
+ @Override
+ public Object invoke(MethodInvocation invocation) throws Throwable {
+
+ Method method = invocation.getMethod();
+ Object[] arguments = invocation.getArguments();
+
if (isCustomMethodInvocation(invocation)) {
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
return executeMethodOn(customImplementation, actualMethod, arguments);
}
- if (hasQueryFor(method)) {
- return queries.get(method).execute(arguments);
- }
-
// Lookup actual method as it might be redeclared in the interface
// and we have to use the repository instance nevertheless
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
@@ -497,7 +567,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
* @return
* @throws Throwable
*/
- private Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {
+ protected Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {
try {
return method.invoke(target, parameters);
@@ -508,16 +578,6 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
throw new IllegalStateException("Should not occur!");
}
- /**
- * Returns whether we know of a query to execute for the given {@link Method};
- *
- * @param method
- * @return
- */
- private boolean hasQueryFor(Method method) {
- return queries.containsKey(method);
- }
-
/**
* Returns whether the given {@link MethodInvocation} is considered to be targeted as an invocation of a custom
* method.
@@ -535,6 +595,69 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
}
}
+ /**
+ * Method interceptor that converts parameters before invoking a method.
+ *
+ * @author Mark Paluch
+ */
+ public class ConvertingImplementationMethodExecutionInterceptor extends ImplementationMethodExecutionInterceptor {
+
+ private final ConversionService conversionService;
+
+ /**
+ * @param repositoryInformation
+ * @param customImplementation
+ * @param target
+ * @param conversionService
+ */
+ public ConvertingImplementationMethodExecutionInterceptor(RepositoryInformation repositoryInformation,
+ Object customImplementation, Object target, ConversionService conversionService) {
+ super(repositoryInformation, customImplementation, target);
+ this.conversionService = conversionService;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.repository.core.support.RepositoryFactorySupport.ImplementationMethodExecutionInterceptor#executeMethodOn(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
+ */
+ @Override
+ protected Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {
+ return super.executeMethodOn(target, method, convertParameters(method.getParameterTypes(), parameters));
+ }
+
+ /**
+ * @param parameterTypes
+ * @param parameters
+ * @return
+ */
+ private Object[] convertParameters(Class>[] parameterTypes, Object[] parameters) {
+
+ if (parameters.length == 0) {
+ return parameters;
+ }
+
+ Object[] result = new Object[parameters.length];
+
+ for (int i = 0; i < parameters.length; i++) {
+
+ if (parameters[i] == null) {
+ continue;
+ }
+
+ if (parameterTypes[i].isAssignableFrom(parameters[i].getClass())
+ || !conversionService.canConvert(parameters[i].getClass(), parameterTypes[i])) {
+
+ result[i] = parameters[i];
+
+ } else {
+ result[i] = conversionService.convert(parameters[i], parameterTypes[i]);
+ }
+
+ }
+
+ return result;
+ }
+ }
+
/**
* {@link QueryCreationListener} collecting the {@link QueryMethod}s created for all query methods of the repository
* interface.
diff --git a/src/main/java/org/springframework/data/repository/query/Parameter.java b/src/main/java/org/springframework/data/repository/query/Parameter.java
index f141e58a5..8b3de4a55 100644
--- a/src/main/java/org/springframework/data/repository/query/Parameter.java
+++ b/src/main/java/org/springframework/data/repository/query/Parameter.java
@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
* Class to abstract a single parameter of a query method. It is held in the context of a {@link Parameters} instance.
*
* @author Oliver Gierke
+ * @author Mark Paluch
*/
public class Parameter {
@@ -205,7 +206,30 @@ public class Parameter {
}
/**
- * Returns the component type if the given {@link MethodParameter} is a wrapper type.
+ * Returns whether the {@link MethodParameter} is wrapped in a wrapper type.
+ *
+ * @param parameter must not be {@literal null}.
+ * @return
+ * @see QueryExecutionConverters
+ */
+ private static boolean isWrapped(MethodParameter parameter) {
+ return QueryExecutionConverters.supports(parameter.getParameterType());
+ }
+
+ /**
+ * Returns whether the {@link MethodParameter} should be unwrapped.
+ *
+ * @param parameter must not be {@literal null}.
+ * @return
+ * @see QueryExecutionConverters
+ */
+ private static boolean shouldUnwrap(MethodParameter parameter) {
+ return QueryExecutionConverters.supportsUnwrapping(parameter.getParameterType());
+ }
+
+ /**
+ * Returns the component type if the given {@link MethodParameter} is a wrapper type and the wrapper should be
+ * unwrapped.
*
* @param parameter must not be {@literal null}.
* @return
@@ -214,7 +238,10 @@ public class Parameter {
Class> originalType = parameter.getParameterType();
- return QueryExecutionConverters.supports(originalType)
- ? ResolvableType.forMethodParameter(parameter).getGeneric(0).getRawClass() : originalType;
+ if (isWrapped(parameter) && shouldUnwrap(parameter)) {
+ return ResolvableType.forMethodParameter(parameter).getGeneric(0).getRawClass();
+ }
+
+ return originalType;
}
}
diff --git a/src/main/java/org/springframework/data/repository/query/ReactiveWrapperConverters.java b/src/main/java/org/springframework/data/repository/query/ReactiveWrapperConverters.java
new file mode 100644
index 000000000..6012f194b
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/query/ReactiveWrapperConverters.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.query;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import org.reactivestreams.Publisher;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.convert.support.GenericConversionService;
+import org.springframework.data.repository.util.QueryExecutionConverters;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import rx.Observable;
+import rx.Single;
+
+/**
+ * Conversion support for reactive wrapper types.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+public abstract class ReactiveWrapperConverters {
+
+ private static final boolean PROJECT_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.converter.DependencyUtils",
+ QueryExecutionConverters.class.getClassLoader());
+ private static final boolean RXJAVA_SINGLE_PRESENT = ClassUtils.isPresent("rx.Single",
+ QueryExecutionConverters.class.getClassLoader());
+ private static final boolean RXJAVA_OBSERVABLE_PRESENT = ClassUtils.isPresent("rx.Observable",
+ QueryExecutionConverters.class.getClassLoader());
+
+ private static final List> REACTIVE_WRAPPERS = new ArrayList<>();
+ private static final GenericConversionService GENERIC_CONVERSION_SERVICE = new GenericConversionService();
+
+ static {
+
+ if (PROJECT_REACTOR_PRESENT) {
+ REACTIVE_WRAPPERS.add(FluxWrapper.INSTANCE);
+ REACTIVE_WRAPPERS.add(MonoWrapper.INSTANCE);
+ REACTIVE_WRAPPERS.add(PublisherWrapper.INSTANCE);
+ }
+
+ if (RXJAVA_SINGLE_PRESENT) {
+ REACTIVE_WRAPPERS.add(SingleWrapper.INSTANCE);
+ }
+
+ if (RXJAVA_OBSERVABLE_PRESENT) {
+ REACTIVE_WRAPPERS.add(ObservableWrapper.INSTANCE);
+ }
+
+ QueryExecutionConverters.registerConvertersIn(GENERIC_CONVERSION_SERVICE);
+ }
+
+ private ReactiveWrapperConverters() {
+
+ }
+
+ /**
+ * Returns whether the given type is a supported wrapper type.
+ *
+ * @param type must not be {@literal null}.
+ * @return
+ */
+ public static boolean supports(Class> type) {
+ return assignableStream(type).isPresent();
+ }
+
+ /**
+ * Returns whether the type is a single-like wrapper.
+ *
+ * @param type must not be {@literal null}.
+ * @return
+ * @see Single
+ * @see Mono
+ */
+ public static boolean isSingleLike(Class> type) {
+ return assignableStream(type).map(wrapper -> wrapper.getMultiplicity() == Multiplicity.ONE).orElse(false);
+ }
+
+ /**
+ * Returns whether the type is a collection/multi-element-like wrapper.
+ *
+ * @param type must not be {@literal null}.
+ * @return
+ * @see Observable
+ * @see Flux
+ * @see Publisher
+ */
+ public static boolean isCollectionLike(Class> type) {
+ return assignableStream(type).map(wrapper -> wrapper.getMultiplicity() == Multiplicity.MANY).orElse(false);
+ }
+
+ /**
+ * Casts or converts the given wrapper type into a different wrapper type.
+ *
+ * @param stream the stream, must not be {@literal null}.
+ * @param expectedWrapperType must not be {@literal null}.
+ * @return
+ */
+ public static T toWrapper(Object stream, Class extends T> expectedWrapperType) {
+
+ Assert.notNull(stream, "Stream must not be null!");
+ Assert.notNull(expectedWrapperType, "Converter must not be null!");
+
+ if (expectedWrapperType.isAssignableFrom(stream.getClass())) {
+ return (T) stream;
+ }
+
+ return GENERIC_CONVERSION_SERVICE.convert(stream, expectedWrapperType);
+ }
+
+ /**
+ * Maps elements of a reactive element stream to other elements.
+ *
+ * @param stream must not be {@literal null}.
+ * @param converter must not be {@literal null}.
+ * @return
+ */
+ public static T map(Object stream, Converter