Browse Source

DATACMNS-1508 - Support for Kotlin Coroutines repositories.

We now support Kotlin Coroutines repositories backed by reactive repositories. Coroutine repositories can either invoke reactive query methods or implemented methods that are backed either by methods returning a reactive wrapper or that are native suspended functions.

Exclude Coroutines Continuation from bindable parameters and name discovery and do not unwrap results for suspended functions returning a reactive type.

interface CoroutinesRepository : CoroutineCrudRepository<User, String> {

    suspend fun findOne(id: String): User

    fun findByFirstname(firstname: String): Flow<User>
}

Original pull request: #415.
pull/428/head
Mark Paluch 6 years ago
parent
commit
ce7683c672
No known key found for this signature in database
GPG Key ID: 51A00FA751B91849
  1. 22
      pom.xml
  2. 30
      src/main/asciidoc/kotlin-coroutines.adoc
  3. 109
      src/main/java/org/springframework/data/repository/core/support/ImplementationInvocationMetadata.java
  4. 27
      src/main/java/org/springframework/data/repository/core/support/MethodLookups.java
  5. 23
      src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java
  6. 97
      src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
  7. 22
      src/main/java/org/springframework/data/repository/query/Parameter.java
  8. 2
      src/main/java/org/springframework/data/repository/reactive/package-info.java
  9. 65
      src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java
  10. 19
      src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java
  11. 171
      src/main/java/org/springframework/data/util/KotlinReflectionUtils.java
  12. 90
      src/main/java/org/springframework/data/util/ReflectionUtils.java
  13. 139
      src/main/kotlin/org/springframework/data/repository/kotlin/CoCrudRepository.kt
  14. 42
      src/main/kotlin/org/springframework/data/repository/kotlin/CoSortingRepository.kt
  15. 5
      src/main/kotlin/org/springframework/data/repository/kotlin/package-info.java
  16. 108
      src/test/java/org/springframework/data/repository/core/support/DummyReactiveRepositoryFactory.java
  17. 8
      src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java
  18. 12
      src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java
  19. 77
      src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryCustomImplementationUnitTests.kt
  20. 170
      src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryUnitTests.kt
  21. 46
      src/test/kotlin/org/springframework/data/repository/query/ParameterUnitTests.kt

22
pom.xml

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@ -265,6 +267,24 @@ @@ -265,6 +267,24 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactive</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>

30
src/main/asciidoc/kotlin-coroutines.adoc

@ -11,7 +11,7 @@ Spring Data modules provide support for Coroutines on the following scope: @@ -11,7 +11,7 @@ Spring Data modules provide support for Coroutines on the following scope:
[[kotlin.coroutines.dependencies]]
== Dependencies
Coroutines support is enabled when `kotlinx-coroutines-core` and `kotlinx-coroutines-reactor` dependencies are in the classpath:
Coroutines support is enabled when `kotlinx-coroutines-core`, `kotlinx-coroutines-reactive` and `kotlinx-coroutines-reactor` dependencies are in the classpath:
.Dependencies to add in Maven pom.xml
====
@ -22,6 +22,11 @@ Coroutines support is enabled when `kotlinx-coroutines-core` and `kotlinx-corout @@ -22,6 +22,11 @@ Coroutines support is enabled when `kotlinx-coroutines-core` and `kotlinx-corout
<artifactId>kotlinx-coroutines-core</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
@ -51,3 +56,26 @@ https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coro @@ -51,3 +56,26 @@ https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coro
* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html[`map` operator] supports asynchronous operation (no need for `flatMap`) since it takes a suspending function parameter
Read this blog post about https://spring.io/blog/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow[Going Reactive with Spring, Coroutines and Kotlin Flow] for more details, including how to run code concurrently with Coroutines.
[[kotlin.coroutines.repositories]]
== Repositories
Here is an example of a Coroutines repository:
====
[source,kotlin]
----
interface CoroutinesRepository : CoCrudRepository<User, String> {
suspend fun findOne(id: String): User
fun findByFirstname(firstname: String): Flow<User>
}
----
====
Coroutines repositories are built on reactive repositories to expose the non-blocking nature of data access through Kotlin's Coroutines.
Methods on a Coroutines repository can be backed either by a query method or a custom implementation.
Invoking a custom implementation method propagates the Coroutines invocation to the actual implementation method if the custom method is `suspend`able without requiring the implementation method to return a reactive type such as `Mono` or `Flux`.
NOTE: Coroutines repositories are only discovered when the repository extends the `CoCrudRepository` interface.

109
src/main/java/org/springframework/data/repository/core/support/ImplementationInvocationMetadata.java

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
/*
* Copyright 2019 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
*
* https://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 kotlin.coroutines.Continuation;
import kotlin.reflect.KFunction;
import kotlinx.coroutines.reactive.AwaitKt;
import java.lang.reflect.Method;
import org.reactivestreams.Publisher;
import org.springframework.core.KotlinDetector;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.lang.Nullable;
/**
* Metadata for a implementation {@link Method} invocation. This value object encapsulates whether the called and the
* backing method are regular methods or suspendable Kotlin coroutines methods. It also allows invocation of suspendable
* methods by backing the invocation using methods returning reactive types.
*
* @author Mark Paluch
* @since 2.3
*/
class ImplementationInvocationMetadata {
private final boolean suspendedDeclaredMethod;
private final boolean suspendedBaseClassMethod;
private final boolean reactiveBaseClassMethod;
ImplementationInvocationMetadata(Method declaredMethod, Method baseClassMethod) {
if (!KotlinDetector.isKotlinReflectPresent()) {
suspendedDeclaredMethod = false;
suspendedBaseClassMethod = false;
reactiveBaseClassMethod = false;
return;
}
KFunction<?> declaredFunction = ReflectionUtils.isKotlinClass(declaredMethod.getDeclaringClass())
? KotlinReflectionUtils.findKotlinFunction(declaredMethod)
: null;
KFunction<?> baseClassFunction = ReflectionUtils.isKotlinClass(baseClassMethod.getDeclaringClass())
? KotlinReflectionUtils.findKotlinFunction(baseClassMethod)
: null;
suspendedDeclaredMethod = declaredFunction != null && declaredFunction.isSuspend();
suspendedBaseClassMethod = baseClassFunction != null && baseClassFunction.isSuspend();
this.reactiveBaseClassMethod = !suspendedBaseClassMethod
&& ReactiveWrapperConverters.supports(baseClassMethod.getReturnType());
}
@Nullable
public Object invoke(Method methodToCall, Object instance, Object[] args) throws Throwable {
if (suspendedDeclaredMethod && !suspendedBaseClassMethod && reactiveBaseClassMethod) {
return invokeReactiveToSuspend(methodToCall, instance, args);
}
return methodToCall.invoke(instance, args);
}
@Nullable
@SuppressWarnings({ "unchecked", "ConstantConditions" })
private Object invokeReactiveToSuspend(Method methodToCall, Object instance, Object[] args)
throws ReflectiveOperationException {
Object[] invocationArguments = new Object[args.length - 1];
System.arraycopy(args, 0, invocationArguments, 0, invocationArguments.length);
Object result = methodToCall.invoke(instance, invocationArguments);
Publisher<?> publisher;
if (result instanceof Publisher) {
publisher = (Publisher<?>) result;
} else {
publisher = ReactiveWrapperConverters.toWrapper(result, Publisher.class);
}
return AwaitKt.awaitFirstOrNull(publisher, (Continuation) args[args.length - 1]);
}
boolean canInvoke(Method invokedMethod, Method backendMethod) {
if (suspendedDeclaredMethod == suspendedBaseClassMethod) {
return invokedMethod.getParameterCount() == backendMethod.getParameterCount();
}
if (suspendedDeclaredMethod && reactiveBaseClassMethod) {
return invokedMethod.getParameterCount() - 1 == backendMethod.getParameterCount();
}
return false;
}
}

27
src/main/java/org/springframework/data/repository/core/support/MethodLookups.java

@ -331,23 +331,22 @@ interface MethodLookups { @@ -331,23 +331,22 @@ interface MethodLookups {
return Optional.of(candidate)//
.filter(it -> invokedMethod.getName().equals(it.getName()))//
.filter(it -> invokedMethod.getParameterCount() == it.getParameterCount())//
.filter(it -> parametersMatch(it, invokedMethod.getMethod(), predicate));
.filter(it -> parameterCountMatch(invokedMethod, it))//
.filter(it -> parametersMatch(invokedMethod.getMethod(), it, predicate));
}
/**
* 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 baseClassMethod must not be {@literal null}.
* @param declaredMethod must not be {@literal null}.
* @param baseClassMethod must not be {@literal null}.
* @param predicate must not be {@literal null}.
* @return
*/
private static boolean parametersMatch(Method baseClassMethod, Method declaredMethod,
private static boolean parametersMatch(Method declaredMethod, Method baseClassMethod,
Predicate<ParameterOverrideCriteria> predicate) {
return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate);
return methodParameters(declaredMethod, baseClassMethod).allMatch(predicate);
}
/**
@ -378,13 +377,17 @@ interface MethodLookups { @@ -378,13 +377,17 @@ interface MethodLookups {
&& parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType());
}
private static Stream<ParameterOverrideCriteria> methodParameters(Method first, Method second) {
private static boolean parameterCountMatch(InvokedMethod invokedMethod, Method baseClassMethod) {
Assert.isTrue(first.getParameterCount() == second.getParameterCount(), "Method parameter count must be equal!");
ImplementationInvocationMetadata invocationMetadata = new ImplementationInvocationMetadata(
invokedMethod.getMethod(), baseClassMethod);
return invocationMetadata.canInvoke(invokedMethod.getMethod(), baseClassMethod);
}
return IntStream.range(0, first.getParameterCount()) //
.mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(first, index),
new MethodParameter(second, index)));
private static Stream<ParameterOverrideCriteria> methodParameters(Method invokedMethod, Method baseClassMethod) {
return IntStream.range(0, baseClassMethod.getParameterCount()) //
.mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(invokedMethod, index),
new MethodParameter(baseClassMethod, index)));
}
/**
@ -395,8 +398,8 @@ interface MethodLookups { @@ -395,8 +398,8 @@ interface MethodLookups {
@Value(staticConstructor = "of")
static class ParameterOverrideCriteria {
private final MethodParameter base;
private final MethodParameter declared;
private final MethodParameter base;
/**
* @return base method parameter type.

23
src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java

@ -28,6 +28,7 @@ import java.util.Iterator; @@ -28,6 +28,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -201,7 +202,7 @@ public class RepositoryComposition { @@ -201,7 +202,7 @@ public class RepositoryComposition {
ReflectionUtils.makeAccessible(methodToCall);
return fragments.invoke(methodToCall, argumentConverter.apply(methodToCall, args));
return fragments.invoke(method, methodToCall, argumentConverter.apply(methodToCall, args));
}
/**
@ -250,6 +251,7 @@ public class RepositoryComposition { @@ -250,6 +251,7 @@ public class RepositoryComposition {
static final RepositoryFragments EMPTY = new RepositoryFragments(Collections.emptyList());
private final Map<Method, RepositoryFragment<?>> fragmentCache = new ConcurrentReferenceHashMap<>();
private final Map<Method, ImplementationInvocationMetadata> invocationMetadataCache = new ConcurrentHashMap<>();
private final List<RepositoryFragment<?>> fragments;
/**
@ -354,21 +356,30 @@ public class RepositoryComposition { @@ -354,21 +356,30 @@ public class RepositoryComposition {
/**
* Invoke {@link Method} by resolving the
*
* @param method
* @param invokedMethod invoked method as per invocation on the interface.
* @param methodToCall backend method that is backing the call.
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Method method, Object[] args) throws Throwable {
@Nullable
public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) throws Throwable {
RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(method, this::findImplementationFragment);
RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment);
Optional<?> optional = fragment.getImplementation();
if (!optional.isPresent()) {
throw new IllegalArgumentException(String.format("No implementation found for method %s", method));
throw new IllegalArgumentException(String.format("No implementation found for method %s", methodToCall));
}
ImplementationInvocationMetadata invocationMetadata = invocationMetadataCache.get(invokedMethod);
if (invocationMetadata == null) {
invocationMetadata = new ImplementationInvocationMetadata(invokedMethod, methodToCall);
invocationMetadataCache.put(invokedMethod, invocationMetadata);
}
return method.invoke(optional.get(), args);
return invocationMetadata.invoke(methodToCall, optional.get(), args);
}
private RepositoryFragment<?> findImplementationFragment(Method key) {

97
src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java

@ -15,6 +15,9 @@ @@ -15,6 +15,9 @@
*/
package org.springframework.data.repository.core.support;
import kotlin.coroutines.Continuation;
import kotlin.reflect.KFunction;
import kotlinx.coroutines.reactive.AwaitKt;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
@ -35,6 +38,7 @@ import java.util.stream.Collectors; @@ -35,6 +38,7 @@ import java.util.stream.Collectors;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.reactivestreams.Publisher;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
@ -43,6 +47,7 @@ import org.springframework.beans.BeansException; @@ -43,6 +47,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
@ -64,6 +69,7 @@ import org.springframework.data.repository.util.ClassUtils; @@ -64,6 +69,7 @@ import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.Pair;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.lang.Nullable;
@ -85,35 +91,35 @@ import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; @@ -85,35 +91,35 @@ import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
@Slf4j
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware {
private static final BiFunction<Method, Object[], Object[]> REACTIVE_ARGS_CONVERTER = (method, o) -> {
private static final BiFunction<Method, Object[], Object[]> REACTIVE_ARGS_CONVERTER = (method, args) -> {
if (ReactiveWrappers.isAvailable()) {
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] converted = new Object[o.length];
for (int i = 0; i < parameterTypes.length; i++) {
Object[] converted = new Object[args.length];
for (int i = 0; i < args.length; i++) {
Class<?> parameterType = parameterTypes[i];
Object value = o[i];
Object value = args[i];
Object convertedArg = value;
if (value == null) {
continue;
}
Class<?> parameterType = parameterTypes.length > i ? parameterTypes[i] : null;
if (!parameterType.isAssignableFrom(value.getClass())
&& ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) {
if (value != null && parameterType != null) {
if (!parameterType.isAssignableFrom(value.getClass())
&& ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) {
converted[i] = ReactiveWrapperConverters.toWrapper(value, parameterType);
} else {
converted[i] = value;
convertedArg = ReactiveWrapperConverters.toWrapper(value, parameterType);
}
}
converted[i] = convertedArg;
}
return converted;
}
return o;
return args;
};
final static GenericConversionService CONVERSION_SERVICE = new DefaultConversionService();
@ -534,6 +540,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -534,6 +540,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
public class QueryExecutorMethodInterceptor implements MethodInterceptor {
private final Map<Method, RepositoryQuery> queries;
private final Map<Method, QueryMethodInvoker> invocationMetadataCache = new ConcurrentReferenceHashMap<>();
private final QueryExecutionResultHandler resultHandler;
/**
@ -615,7 +622,16 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -615,7 +622,16 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
Method method = invocation.getMethod();
if (hasQueryFor(method)) {
return queries.get(method).execute(invocation.getArguments());
QueryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);
if (invocationMetadata == null) {
invocationMetadata = new QueryMethodInvoker(method);
invocationMetadataCache.put(method, invocationMetadata);
}
RepositoryQuery repositoryQuery = queries.get(method);
return invocationMetadata.invoke(repositoryQuery, invocation.getArguments());
}
return invocation.proceed();
@ -711,4 +727,55 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @@ -711,4 +727,55 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
this.compositionHash = composition.hashCode();
}
}
static class QueryMethodInvoker {
private final boolean suspendedDeclaredMethod;
private final Class<?> returnedType;
private final boolean returnsReactiveType;
QueryMethodInvoker(Method invokedMethod) {
this.suspendedDeclaredMethod = KotlinDetector.isKotlinReflectPresent() && isSuspendedMethod(invokedMethod);
this.returnedType = this.suspendedDeclaredMethod ? KotlinReflectionUtils.getReturnType(invokedMethod)
: invokedMethod.getReturnType();
this.returnsReactiveType = ReactiveWrappers.supports(returnedType);
}
private static boolean isSuspendedMethod(Method invokedMethod) {
KFunction<?> invokedFunction = ReflectionUtils.isKotlinClass(invokedMethod.getDeclaringClass())
? KotlinReflectionUtils.findKotlinFunction(invokedMethod)
: null;
return invokedFunction != null && invokedFunction.isSuspend();
}
@Nullable
public Object invoke(RepositoryQuery query, Object[] args) {
return suspendedDeclaredMethod ? invokeReactiveToSuspend(query, args) : query.execute(args);
}
@Nullable
@SuppressWarnings({ "unchecked", "ConstantConditions" })
private Object invokeReactiveToSuspend(RepositoryQuery query, Object[] args) {
Continuation<Object> continuation = (Continuation) args[args.length - 1];
args[args.length - 1] = null;
Object result = query.execute(args);
if (returnsReactiveType) {
return ReactiveWrapperConverters.toWrapper(result, returnedType);
}
Publisher<?> publisher;
if (result instanceof Publisher) {
publisher = (Publisher<?>) result;
} else {
publisher = ReactiveWrapperConverters.toWrapper(result, Publisher.class);
}
return AwaitKt.awaitFirstOrNull(publisher, continuation);
}
}
}

22
src/main/java/org/springframework/data/repository/query/Parameter.java

@ -18,7 +18,9 @@ package org.springframework.data.repository.query; @@ -18,7 +18,9 @@ package org.springframework.data.repository.query;
import static java.lang.String.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -31,6 +33,7 @@ import org.springframework.data.util.ClassTypeInformation; @@ -31,6 +33,7 @@ import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Class to abstract a single parameter of a query method. It is held in the context of a {@link Parameters} instance.
@ -41,7 +44,7 @@ import org.springframework.util.Assert; @@ -41,7 +44,7 @@ import org.springframework.util.Assert;
*/
public class Parameter {
static final List<Class<?>> TYPES = Arrays.asList(Pageable.class, Sort.class);
static final List<Class<?>> TYPES;
private static final String NAMED_PARAMETER_TEMPLATE = ":%s";
private static final String POSITION_PARAMETER_TEMPLATE = "?%s";
@ -51,6 +54,21 @@ public class Parameter { @@ -51,6 +54,21 @@ public class Parameter {
private final boolean isDynamicProjectionParameter;
private final Lazy<Optional<String>> name;
static {
List<Class<?>> types = new ArrayList<>(Arrays.asList(Pageable.class, Sort.class));
try {
// consider Kotlin Coroutines Continuation a special parameter. That parameter is synthetic and should not get
// bound to any query.
types.add(ClassUtils.forName("kotlin.coroutines.Continuation", Parameter.class.getClassLoader()));
} catch (ClassNotFoundException e) {
// ignore
}
TYPES = Collections.unmodifiableList(types);
}
/**
* Creates a new {@link Parameter} for the given {@link MethodParameter}.
*
@ -63,7 +81,7 @@ public class Parameter { @@ -63,7 +81,7 @@ public class Parameter {
this.parameter = parameter;
this.parameterType = potentiallyUnwrapParameterType(parameter);
this.isDynamicProjectionParameter = isDynamicProjectionParameter(parameter);
this.name = Lazy.of(() -> {
this.name = TYPES.contains(parameter.getParameterType()) ? Lazy.of(Optional.empty()) : Lazy.of(() -> {
Param annotation = parameter.getParameterAnnotation(Param.class);
return Optional.ofNullable(annotation == null ? parameter.getParameterName() : annotation.value());
});

2
src/main/java/org/springframework/data/repository/reactive/package-info.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/**
* Support for reactive repository.
* Support for reactive repositories.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.repository.reactive;

65
src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java

@ -20,6 +20,8 @@ import static org.springframework.data.repository.util.ReactiveWrapperConverters @@ -20,6 +20,8 @@ import static org.springframework.data.repository.util.ReactiveWrapperConverters
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import kotlinx.coroutines.flow.Flow;
import kotlinx.coroutines.reactive.ReactiveFlowKt;
import lombok.experimental.UtilityClass;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -34,10 +36,14 @@ import java.util.function.Function; @@ -34,10 +36,14 @@ import java.util.function.Function;
import javax.annotation.Nonnull;
import org.reactivestreams.Publisher;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.repository.util.ReactiveWrappers.ReactiveLibrary;
@ -156,6 +162,12 @@ public class ReactiveWrapperConverters { @@ -156,6 +162,12 @@ public class ReactiveWrapperConverters {
conversionService.addConverter(RxJava2ObservableToSingleConverter.INSTANCE);
conversionService.addConverter(RxJava2ObservableToMaybeConverter.INSTANCE);
}
if (ReactiveWrappers.isAvailable(ReactiveLibrary.KOTLIN_COROUTINES)) {
conversionService.addConverter(PublisherToFlowConverter.INSTANCE);
}
conversionService.addConverterFactory(ReactiveAdapterConverterFactory.INSTANCE);
}
return conversionService;
@ -492,6 +504,27 @@ public class ReactiveWrapperConverters { @@ -492,6 +504,27 @@ public class ReactiveWrapperConverters {
}
}
// -------------------------------------------------------------------------
// Coroutine converters
// -------------------------------------------------------------------------
/**
* A {@link Converter} to convert a {@link Publisher} to {@link Flow}.
*
* @author Mark Paluch
* @author 2.3
*/
private enum PublisherToFlowConverter implements Converter<Publisher<?>, Flow<?>> {
INSTANCE;
@Nonnull
@Override
public Flow<?> convert(Publisher<?> source) {
return ReactiveFlowKt.asFlow(source);
}
}
// -------------------------------------------------------------------------
// RxJava 1 converters
// -------------------------------------------------------------------------
@ -1064,6 +1097,38 @@ public class ReactiveWrapperConverters { @@ -1064,6 +1097,38 @@ public class ReactiveWrapperConverters {
}
}
/**
* A {@link ConverterFactory} that adapts between reactive types using {@link ReactiveAdapterRegistry}.
*/
private enum ReactiveAdapterConverterFactory implements ConverterFactory<Object, Object>, ConditionalConverter {
INSTANCE;
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return isSupported(sourceType) || isSupported(targetType);
}
private boolean isSupported(TypeDescriptor typeDescriptor) {
return REACTIVE_ADAPTER_REGISTRY != null
&& REACTIVE_ADAPTER_REGISTRY.getAdapter(typeDescriptor.getType()) != null;
}
@Override
@SuppressWarnings({ "ConstantConditions", "unchecked" })
public <T> Converter<Object, T> getConverter(Class<T> targetType) {
return source -> {
Publisher<?> publisher = source instanceof Publisher ? (Publisher<?>) source
: REACTIVE_ADAPTER_REGISTRY.getAdapter(Publisher.class, source).toPublisher(source);
ReactiveAdapter adapter = REACTIVE_ADAPTER_REGISTRY.getAdapter(targetType);
return (T) adapter.fromPublisher(publisher);
};
}
}
/**
* Holder for delayed initialization of {@link ReactiveAdapterRegistry}.
*

19
src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java

@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
*/
package org.springframework.data.repository.util;
import kotlin.Unit;
import kotlinx.coroutines.flow.Flow;
import lombok.experimental.UtilityClass;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -30,6 +32,7 @@ import java.util.Optional; @@ -30,6 +32,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.core.ReactiveTypeDescriptor;
import org.springframework.data.util.ProxyUtils;
import org.springframework.data.util.ReflectionUtils;
@ -71,6 +74,11 @@ public class ReactiveWrappers { @@ -71,6 +74,11 @@ public class ReactiveWrappers {
private static final boolean RXJAVA2_PRESENT = ClassUtils.isPresent("io.reactivex.Flowable",
ReactiveWrappers.class.getClassLoader());
private static final boolean KOTLIN_COROUTINES_PRESENT = ClassUtils.isPresent("kotlinx.coroutines.flow.Flow",
ReactiveWrappers.class.getClassLoader())
&& ClassUtils.isPresent("kotlinx.coroutines.reactive.ReactiveFlowKt", ReactiveWrappers.class.getClassLoader())
&& ClassUtils.isPresent("kotlinx.coroutines.reactor.ReactorFlowKt", ReactiveWrappers.class.getClassLoader());
private static final Collection<ReactiveTypeDescriptor> REACTIVE_WRAPPERS;
/**
@ -79,7 +87,7 @@ public class ReactiveWrappers { @@ -79,7 +87,7 @@ public class ReactiveWrappers {
* @author Mark Paluch
*/
static enum ReactiveLibrary {
PROJECT_REACTOR, RXJAVA1, RXJAVA2;
PROJECT_REACTOR, RXJAVA1, RXJAVA2, KOTLIN_COROUTINES;
}
static {
@ -111,6 +119,13 @@ public class ReactiveWrappers { @@ -111,6 +119,13 @@ public class ReactiveWrappers {
reactiveWrappers.add(ReactiveTypeDescriptor.singleOptionalValue(Mono.class, Mono::empty));
reactiveWrappers.add(ReactiveTypeDescriptor.multiValue(Flux.class, Flux::empty));
reactiveWrappers.add(ReactiveTypeDescriptor.multiValue(Publisher.class, Flux::empty));
if (KOTLIN_COROUTINES_PRESENT) {
reactiveWrappers.add(ReactiveTypeDescriptor.multiValue(Flow.class, () -> {
return (Flow<Object>) (flowCollector, continuation) -> Unit.INSTANCE;
}));
}
}
REACTIVE_WRAPPERS = Collections.unmodifiableCollection(reactiveWrappers);
@ -143,6 +158,8 @@ public class ReactiveWrappers { @@ -143,6 +158,8 @@ public class ReactiveWrappers {
return RXJAVA1_PRESENT;
case RXJAVA2:
return RXJAVA2_PRESENT;
case KOTLIN_COROUTINES:
return PROJECT_REACTOR_PRESENT && KOTLIN_COROUTINES_PRESENT;
default:
throw new IllegalArgumentException(String.format("Reactive library %s not supported", reactiveLibrary));
}

171
src/main/java/org/springframework/data/util/KotlinReflectionUtils.java

@ -0,0 +1,171 @@ @@ -0,0 +1,171 @@
/*
* Copyright 2019 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
*
* https://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.util;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KCallable;
import kotlin.reflect.KClass;
import kotlin.reflect.KFunction;
import kotlin.reflect.KMutableProperty;
import kotlin.reflect.KProperty;
import kotlin.reflect.KType;
import kotlin.reflect.jvm.KTypesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
/**
* Reflection utility methods specific to Kotlin reflection. Requires Kotlin classes to be present to avoid linkage
* errors.
*
* @author Mark Paluch
* @since 2.3
*/
public final class KotlinReflectionUtils {
private KotlinReflectionUtils() {}
/**
* Returns a {@link KFunction} instance corresponding to the given Java {@link Method} instance, or {@code null} if
* this method cannot be represented by a Kotlin function.
*
* @param method the method to look up.
* @return the {@link KFunction} or {@code null} if the method cannot be looked up.
*/
@Nullable
public static KFunction<?> findKotlinFunction(Method method) {
KFunction<?> kotlinFunction = ReflectJvmMapping.getKotlinFunction(method);
if (kotlinFunction == null) {
// Fallback to own lookup because there's no public Kotlin API for that kind of lookup until
// https://youtrack.jetbrains.com/issue/KT-20768 gets resolved.
return findKFunction(method).orElse(null);
}
return kotlinFunction;
}
/**
* Returns the {@link Class return type} of a Kotlin {@link Method}. Supports regular and suspended methods.
*
* @param method the method to inspect, typically any synthetic JVM {@link Method}.
* @return return type of the method.
*/
public static Class<?> getReturnType(Method method) {
KFunction<?> kotlinFunction = KotlinReflectionUtils.findKotlinFunction(method);
if (kotlinFunction == null) {
throw new IllegalArgumentException(String.format("Cannot resolve %s to a KFunction!", method));
}
return JvmClassMappingKt.getJavaClass(KTypesJvm.getJvmErasure(kotlinFunction.getReturnType()));
}
/**
* Returns {@literal} whether the given {@link MethodParameter} is nullable. Its declaring method can reference a
* Kotlin function, property or interface property.
*
* @return {@literal true} if {@link MethodParameter} is nullable.
* @since 2.0.1
*/
static boolean isNullable(MethodParameter parameter) {
Method method = parameter.getMethod();
if (method == null) {
throw new IllegalStateException(String.format("Cannot obtain method from parameter %s!", parameter));
}
KFunction<?> kotlinFunction = findKotlinFunction(method);
if (kotlinFunction == null) {
throw new IllegalArgumentException(String.format("Cannot resolve %s to a Kotlin function!", parameter));
}
// Special handling for Coroutines
if (kotlinFunction.isSuspend() && isLast(parameter)) {
return false;
}
// see https://github.com/spring-projects/spring-framework/issues/23991
if (kotlinFunction.getParameters().size() > parameter.getParameterIndex() + 1) {
KType type = parameter.getParameterIndex() == -1 //
? kotlinFunction.getReturnType() //
: kotlinFunction.getParameters().get(parameter.getParameterIndex() + 1).getType();
return type.isMarkedNullable();
}
return true;
}
private static boolean isLast(MethodParameter parameter) {
return parameter.getParameterIndex() == parameter.getMethod().getParameterCount() - 1;
}
/**
* Lookup a {@link Method} to a {@link KFunction}.
*
* @param method the JVM {@link Method} to look up.
* @return {@link Optional} wrapping a possibly existing {@link KFunction}.
*/
private static Optional<? extends KFunction<?>> findKFunction(Method method) {
KClass<?> kotlinClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass());
return kotlinClass.getMembers() //
.stream() //
.flatMap(KotlinReflectionUtils::toKFunctionStream) //
.filter(it -> isSame(it, method)) //
.findFirst();
}
private static Stream<? extends KFunction<?>> toKFunctionStream(KCallable<?> it) {
if (it instanceof KMutableProperty<?>) {
KMutableProperty<?> property = (KMutableProperty<?>) it;
return Stream.of(property.getGetter(), property.getSetter());
}
if (it instanceof KProperty<?>) {
KProperty<?> property = (KProperty<?>) it;
return Stream.of(property.getGetter());
}
if (it instanceof KFunction<?>) {
return Stream.of((KFunction<?>) it);
}
return Stream.empty();
}
private static boolean isSame(KFunction<?> function, Method method) {
Method javaMethod = ReflectJvmMapping.getJavaMethod(function);
return javaMethod != null && javaMethod.equals(method);
}
}

90
src/main/java/org/springframework/data/util/ReflectionUtils.java

@ -15,14 +15,6 @@ @@ -15,14 +15,6 @@
*/
package org.springframework.data.util;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KCallable;
import kotlin.reflect.KClass;
import kotlin.reflect.KFunction;
import kotlin.reflect.KMutableProperty;
import kotlin.reflect.KProperty;
import kotlin.reflect.KType;
import kotlin.reflect.jvm.ReflectJvmMapping;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.UtilityClass;
@ -449,86 +441,4 @@ public class ReflectionUtils { @@ -449,86 +441,4 @@ public class ReflectionUtils {
throw new IllegalArgumentException(String.format("Primitive type %s not supported!", type));
}
/**
* Reflection utility methods specific to Kotlin reflection.
*/
static class KotlinReflectionUtils {
/**
* Returns {@literal} whether the given {@link MethodParameter} is nullable. Its declaring method can reference a
* Kotlin function, property or interface property.
*
* @return {@literal true} if {@link MethodParameter} is nullable.
* @since 2.0.1
*/
static boolean isNullable(MethodParameter parameter) {
Method method = parameter.getMethod();
if (method == null) {
throw new IllegalStateException(String.format("Cannot obtain method from parameter %s!", parameter));
}
KFunction<?> kotlinFunction = ReflectJvmMapping.getKotlinFunction(method);
if (kotlinFunction == null) {
// Fallback to own lookup because there's no public Kotlin API for that kind of lookup until
// https://youtrack.jetbrains.com/issue/KT-20768 gets resolved.
kotlinFunction = findKFunction(method)//
.orElseThrow(() -> new IllegalArgumentException(
String.format("Cannot resolve %s to a Kotlin function!", parameter)));
}
KType type = parameter.getParameterIndex() == -1 //
? kotlinFunction.getReturnType() //
: kotlinFunction.getParameters().get(parameter.getParameterIndex() + 1).getType();
return type.isMarkedNullable();
}
/**
* Lookup a {@link Method} to a {@link KFunction}.
*
* @param method the JVM {@link Method} to look up.
* @return {@link Optional} wrapping a possibly existing {@link KFunction}.
*/
private static Optional<? extends KFunction<?>> findKFunction(Method method) {
KClass<?> kotlinClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass());
return kotlinClass.getMembers() //
.stream() //
.flatMap(KotlinReflectionUtils::toKFunctionStream) //
.filter(it -> isSame(it, method)) //
.findFirst();
}
private static Stream<? extends KFunction<?>> toKFunctionStream(KCallable<?> it) {
if (it instanceof KMutableProperty<?>) {
KMutableProperty<?> property = (KMutableProperty<?>) it;
return Stream.of(property.getGetter(), property.getSetter());
}
if (it instanceof KProperty<?>) {
KProperty<?> property = (KProperty<?>) it;
return Stream.of(property.getGetter());
}
if (it instanceof KFunction<?>) {
return Stream.of((KFunction<?>) it);
}
return Stream.empty();
}
private static boolean isSame(KFunction<?> function, Method method) {
Method javaMethod = ReflectJvmMapping.getJavaMethod(function);
return javaMethod != null && javaMethod.equals(method);
}
}
}

139
src/main/kotlin/org/springframework/data/repository/kotlin/CoCrudRepository.kt

@ -0,0 +1,139 @@ @@ -0,0 +1,139 @@
/*
* Copyright 2019 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
*
* https://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.kotlin
import kotlinx.coroutines.flow.Flow
import org.springframework.data.repository.NoRepositoryBean
import org.springframework.data.repository.Repository
import reactor.core.publisher.Mono
/**
* Interface for generic CRUD operations using Kotlin Coroutines on a repository for a specific type.
*
* @author Mark Paluch
* @since 2.3
* @see Flow
*/
@NoRepositoryBean
interface CoCrudRepository<T, ID> : Repository<T, ID> {
/**
* Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
* entity instance completely.
*
* @param entity must not be null.
* @return the saved entity.
* @throws IllegalArgumentException in case the given entity is null.
*/
suspend fun <S : T> save(entity: S): T
/**
* Saves all given entities.
*
* @param entities must not be null.
* @return [Flow] emitting the saved entities.
* @throws IllegalArgumentException in case the given [entities][Flow] or one of its entities is
* null.
*/
fun <S : T> saveAll(entities: Iterable<S>): Flow<S>
/**
* Saves all given entities.
*
* @param entityStream must not be null.
* @return [Flow] emitting the saved entities.
* @throws IllegalArgumentException in case the given [entityStream][Flow] is null.
*/
fun <S : T> saveAll(entityStream: Flow<S>): Flow<S>
/**
* Retrieves an entity by its id.
*
* @param id must not be null.
* @return [Mono] emitting the entity with the given id or empty if none found.
* @throws IllegalArgumentException in case the given id is null.
*/
suspend fun findById(id: ID): T?
/**
* Returns whether an entity with the given id exists.
*
* @param id must not be null.
* @return true if an entity with the given id exists, false otherwise.
* @throws IllegalArgumentException in case the given id is null.
*/
suspend fun existsById(id: ID): Boolean
/**
* Returns all instances of the type.
*
* @return [Flow] emitting all entities.
*/
fun findAll(): Flow<T>
/**
* Returns all instances of the type `T` with the given IDs.
*
*
* If some or all ids are not found, no entities are returned for these IDs.
*
*
* Note that the order of elements in the result is not guaranteed.
*
* @param ids must not be null nor contain any null values.
* @return [Flow] emitting the found entities. The size can be equal or less than the number of given
* ids.
* @throws IllegalArgumentException in case the given [ids][Iterable] or one of its items is null.
*/
fun findAllById(ids: Iterable<ID>): Flow<T>
/**
* Returns the number of entities available.
*
* @return number of entities.
*/
suspend fun count(): Long
/**
* Deletes the entity with the given id.
*
* @param id must not be null.
* @throws IllegalArgumentException in case the given id is null.
*/
suspend fun deleteById(id: ID)
/**
* Deletes a given entity.
*
* @param entity must not be null.
* @throws IllegalArgumentException in case the given entity is null.
*/
suspend fun delete(entity: T)
/**
* Deletes the given entities.
*
* @param entities must not be null.
* @throws IllegalArgumentException in case the given [entities][Iterable] or one of its entities is
* null.
*/
suspend fun deleteAll(entities: Iterable<T>)
/**
* Deletes all entities managed by the repository.
*/
suspend fun deleteAll()
}

42
src/main/kotlin/org/springframework/data/repository/kotlin/CoSortingRepository.kt

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
/*
* Copyright 2019 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
*
* https://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.kotlin
import kotlinx.coroutines.flow.Flow
import org.springframework.data.domain.Sort
import org.springframework.data.repository.NoRepositoryBean
/**
* Extension of [CoCrudRepository] to provide additional methods to retrieve entities using the sorting
* abstraction.
*
* @author Mark Paluch
* @since 2.3
* @see Flow
* @see Sort
*/
@NoRepositoryBean
interface CoSortingRepository<T, ID> : CoCrudRepository<T, ID> {
/**
* Returns all entities sorted by the given options.
*
* @param sort must not be null.
* @return all entities sorted by the given options.
* @throws IllegalArgumentException in case the given [Sort] is null.
*/
fun findAll(sort: Sort): Flow<T>
}

5
src/main/kotlin/org/springframework/data/repository/kotlin/package-info.java

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
/**
* Support for Kotlin Coroutines repositories.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.repository.kotlin;

108
src/test/java/org/springframework/data/repository/core/support/DummyReactiveRepositoryFactory.java

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
/*
* Copyright 2019 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
*
* https://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.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.Optional;
import org.mockito.Mockito;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
/**
* @author Mark Paluch
*/
public class DummyReactiveRepositoryFactory extends ReactiveRepositoryFactorySupport {
public final DummyRepositoryFactory.MyRepositoryQuery queryOne = mock(DummyRepositoryFactory.MyRepositoryQuery.class);
public final RepositoryQuery queryTwo = mock(RepositoryQuery.class);
public final QueryLookupStrategy strategy = mock(QueryLookupStrategy.class);
@SuppressWarnings("unchecked") private final QuerydslPredicateExecutor<Object> querydsl = mock(
QuerydslPredicateExecutor.class);
private final Object repository;
public DummyReactiveRepositoryFactory(Object repository) {
this.repository = repository;
when(strategy.resolveQuery(Mockito.any(Method.class), Mockito.any(RepositoryMetadata.class),
Mockito.any(ProjectionFactory.class), Mockito.any(NamedQueries.class))).thenReturn(queryOne);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class)
*/
@Override
@SuppressWarnings("unchecked")
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return mock(EntityInformation.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryMetadata)
*/
@Override
protected Object getTargetRepository(RepositoryInformation information) {
return repository;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)
*/
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return repository.getClass();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider)
*/
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(strategy);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryFragments(org.springframework.data.repository.core.RepositoryMetadata)
*/
@Override
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
RepositoryComposition.RepositoryFragments fragments = super.getRepositoryFragments(metadata);
return QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()) //
? fragments.append(RepositoryComposition.RepositoryFragments.just(querydsl)) //
: fragments;
}
}

8
src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java

@ -28,7 +28,6 @@ import org.springframework.data.repository.core.NamedQueries; @@ -28,7 +28,6 @@ import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFactorySupportUnitTests.MyRepositoryQuery;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
@ -109,4 +108,11 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport { @@ -109,4 +108,11 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport {
? fragments.append(RepositoryFragments.just(querydsl)) //
: fragments;
}
/**
* @author Mark Paluch
*/
public static interface MyRepositoryQuery extends RepositoryQuery {
}
}

12
src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java

@ -19,6 +19,7 @@ import static java.util.Collections.*; @@ -19,6 +19,7 @@ import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.repository.core.support.DummyRepositoryFactory.*;
import java.io.Serializable;
import java.lang.reflect.Method;
@ -93,8 +94,7 @@ public class RepositoryFactorySupportUnitTests { @@ -93,8 +94,7 @@ public class RepositoryFactorySupportUnitTests {
Mockito.reset(factory.strategy);
when(factory.strategy.resolveQuery(any(Method.class), any(RepositoryMetadata.class), any(ProjectionFactory.class),
any(NamedQueries.class))).thenReturn(factory.queryOne,
factory.queryTwo);
any(NamedQueries.class))).thenReturn(factory.queryOne, factory.queryTwo);
factory.addQueryCreationListener(listener);
factory.addQueryCreationListener(otherListener);
@ -309,7 +309,7 @@ public class RepositoryFactorySupportUnitTests { @@ -309,7 +309,7 @@ public class RepositoryFactorySupportUnitTests {
assertThatThrownBy( //
() -> repository.findById("")) //
.isInstanceOf(EmptyResultDataAccessException.class) //
.hasMessageContaining("Result must not be null!");
.hasMessageContaining("Result must not be null!");
assertThat(repository.findByUsername("")).isNull();
}
@ -322,7 +322,7 @@ public class RepositoryFactorySupportUnitTests { @@ -322,7 +322,7 @@ public class RepositoryFactorySupportUnitTests {
assertThatThrownBy( //
() -> repository.findByClass(null)) //
.isInstanceOf(IllegalArgumentException.class) //
.hasMessageContaining("must not be null!");
.hasMessageContaining("must not be null!");
}
@Test // DATACMNS-1154
@ -427,10 +427,6 @@ public class RepositoryFactorySupportUnitTests { @@ -427,10 +427,6 @@ public class RepositoryFactorySupportUnitTests {
}
interface MyRepositoryQuery extends RepositoryQuery {
}
interface ReadOnlyRepository<T, ID extends Serializable> extends Repository<T, ID> {
Optional<T> findById(ID id);

77
src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryCustomImplementationUnitTests.kt

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
/*
* Copyright 2019 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
*
* https://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.kotlin
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.springframework.data.repository.core.RepositoryMetadata
import org.springframework.data.repository.core.support.DummyReactiveRepositoryFactory
import org.springframework.data.repository.core.support.RepositoryComposition
import org.springframework.data.repository.core.support.RepositoryFragment
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.data.repository.sample.User
/**
* Unit tests for Coroutine repositories.
*
* @author Mark Paluch
*/
class CoCrudRepositoryCustomImplementationUnitTests {
val backingRepository = mockk<ReactiveCrudRepository<User, String>>()
lateinit var factory: DummyReactiveRepositoryFactory;
lateinit var coRepository: MyCoRepository;
@Before
fun before() {
factory = CustomDummyReactiveRepositoryFactory(backingRepository)
coRepository = factory.getRepository(MyCoRepository::class.java)
}
@Test // DATACMNS-1508
fun shouldInvokeFindAll() {
val result = runBlocking {
coRepository.findOne("foo")
}
assertThat(result).isNotNull()
}
class CustomDummyReactiveRepositoryFactory(repository: Any) : DummyReactiveRepositoryFactory(repository) {
override fun getRepositoryFragments(metadata: RepositoryMetadata): RepositoryComposition.RepositoryFragments {
return super.getRepositoryFragments(metadata).append(RepositoryFragment.implemented(MyCustomCoRepository::class.java, MyCustomCoRepositoryImpl()))
}
}
interface MyCoRepository : CoCrudRepository<User, String>, MyCustomCoRepository
interface MyCustomCoRepository {
suspend fun findOne(id: String): User
}
class MyCustomCoRepositoryImpl : MyCustomCoRepository {
override suspend fun findOne(id: String): User {
return User()
}
}
}

170
src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryUnitTests.kt

@ -0,0 +1,170 @@ @@ -0,0 +1,170 @@
/*
* Copyright 2019 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
*
* https://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.kotlin
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.springframework.data.repository.core.support.DummyReactiveRepositoryFactory
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.data.repository.sample.User
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import rx.Observable
import rx.Single
/**
* Unit tests for Coroutine repositories.
*
* @author Mark Paluch
*/
class CoCrudRepositoryUnitTests {
val backingRepository = mockk<ReactiveCrudRepository<User, String>>()
lateinit var factory: DummyReactiveRepositoryFactory;
lateinit var coRepository: MyCoRepository;
@Before
fun before() {
factory = DummyReactiveRepositoryFactory(backingRepository)
coRepository = factory.getRepository(MyCoRepository::class.java)
}
@Test // DATACMNS-1508
fun shouldInvokeFindAll() {
val sample = User()
every { backingRepository.findAll() }.returns(Flux.just(sample))
val result = runBlocking {
coRepository.findAll().toList()
}
assertThat(result).hasSize(1).containsOnly(sample)
}
@Test // DATACMNS-1508
fun shouldInvokeFindById() {
val sample = User()
every { backingRepository.findById("foo") }.returns(Mono.just(sample))
val result = runBlocking {
coRepository.findById("foo")
}
assertThat(result).isNotNull().isEqualTo(sample)
}
@Test // DATACMNS-1508
fun shouldBridgeQueryMethod() {
val sample = User()
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Mono.just(sample))
val result = runBlocking {
coRepository.findOne("foo")
}
assertThat(result).isNotNull().isEqualTo(sample)
}
@Test // DATACMNS-1508
fun shouldBridgeRxJavaQueryMethod() {
val sample = User()
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Single.just(sample))
val result = runBlocking {
coRepository.findOne("foo")
}
assertThat(result).isNotNull().isEqualTo(sample)
}
@Test // DATACMNS-1508
fun shouldBridgeFlowMethod() {
val sample = User()
Mockito.`when`(factory.queryOne.execute(arrayOf("foo"))).thenReturn(Flux.just(sample), Flux.empty<User>())
val result = runBlocking {
coRepository.findMultiple("foo").toList()
}
assertThat(result).hasSize(1).containsOnly(sample)
val emptyResult = runBlocking {
coRepository.findMultiple("foo").toList()
}
assertThat(emptyResult).isEmpty()
}
@Test // DATACMNS-1508
fun shouldBridgeRxJavaToFlowMethod() {
val sample = User()
Mockito.`when`(factory.queryOne.execute(arrayOf("foo"))).thenReturn(Observable.just(sample))
val result = runBlocking {
coRepository.findMultiple("foo").toList()
}
assertThat(result).hasSize(1).containsOnly(sample)
}
@Test // DATACMNS-1508
fun shouldBridgeSuspendedFlowMethod() {
val sample = User()
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Flux.just(sample), Flux.empty<User>())
val result = runBlocking {
coRepository.findSuspendedMultiple("foo").toList()
}
assertThat(result).hasSize(1).containsOnly(sample)
val emptyResult = runBlocking {
coRepository.findSuspendedMultiple("foo").toList()
}
assertThat(emptyResult).isEmpty()
}
interface MyCoRepository : CoCrudRepository<User, String> {
suspend fun findOne(id: String): User
fun findMultiple(id: String): Flow<User>
suspend fun findSuspendedMultiple(id: String): Flow<User>
}
}

46
src/test/kotlin/org/springframework/data/repository/query/ParameterUnitTests.kt

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
/*
* Copyright 2019 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
*
* https://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 org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.springframework.core.DefaultParameterNameDiscoverer
import org.springframework.core.MethodParameter
import org.springframework.core.ParameterNameDiscoverer
import kotlin.reflect.jvm.javaMethod
/**
* Unit tests for [Parameter].
*
* @author Mark Paluch
*/
class ParameterUnitTests {
@Test // DATACMNS-1508
fun `should consider Continuation a special parameter`() {
val methodParameter = MethodParameter(MyCoroutineRepository::hello.javaMethod, 0)
methodParameter.initParameterNameDiscovery(DefaultParameterNameDiscoverer())
val parameter = Parameter(methodParameter)
assertThat(parameter.name).isEmpty()
assertThat(parameter.isBindable).isFalse()
}
interface MyCoroutineRepository {
suspend fun hello(): Any
}
}
Loading…
Cancel
Save