diff --git a/pom.xml b/pom.xml index ac5bb1204..df8759fd5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -265,6 +267,24 @@ true + + org.jetbrains.kotlinx + kotlinx-coroutines-core + true + + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactive + true + + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + + org.jetbrains.kotlin kotlin-test diff --git a/src/main/asciidoc/kotlin-coroutines.adoc b/src/main/asciidoc/kotlin-coroutines.adoc index 366df0d69..5afeac07d 100644 --- a/src/main/asciidoc/kotlin-coroutines.adoc +++ b/src/main/asciidoc/kotlin-coroutines.adoc @@ -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 kotlinx-coroutines-core + + org.jetbrains.kotlinx + kotlinx-coroutines-reactive + + org.jetbrains.kotlinx kotlinx-coroutines-reactor @@ -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 { + + suspend fun findOne(id: String): User + + fun findByFirstname(firstname: String): Flow +} +---- +==== + +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. diff --git a/src/main/java/org/springframework/data/repository/core/support/ImplementationInvocationMetadata.java b/src/main/java/org/springframework/data/repository/core/support/ImplementationInvocationMetadata.java new file mode 100644 index 000000000..1b43ed9c5 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/ImplementationInvocationMetadata.java @@ -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; + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java index 11f3c95a1..8f1358f03 100644 --- a/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java +++ b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java @@ -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 predicate) { - - return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate); + return methodParameters(declaredMethod, baseClassMethod).allMatch(predicate); } /** @@ -378,13 +377,17 @@ interface MethodLookups { && parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType()); } - private static Stream 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 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 { @Value(staticConstructor = "of") static class ParameterOverrideCriteria { - private final MethodParameter base; private final MethodParameter declared; + private final MethodParameter base; /** * @return base method parameter type. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java index 1228ca5d0..3f5988e21 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -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 { 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 { static final RepositoryFragments EMPTY = new RepositoryFragments(Collections.emptyList()); private final Map> fragmentCache = new ConcurrentReferenceHashMap<>(); + private final Map invocationMetadataCache = new ConcurrentHashMap<>(); private final List> fragments; /** @@ -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)); } - return method.invoke(optional.get(), args); + ImplementationInvocationMetadata invocationMetadata = invocationMetadataCache.get(invokedMethod); + + if (invocationMetadata == null) { + invocationMetadata = new ImplementationInvocationMetadata(invokedMethod, methodToCall); + invocationMetadataCache.put(invokedMethod, invocationMetadata); + } + + return invocationMetadata.invoke(methodToCall, optional.get(), args); } private RepositoryFragment findImplementationFragment(Method key) { 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 d0b23b192..c60d848b0 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 @@ -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; 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; 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; 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; @Slf4j public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware { - private static final BiFunction REACTIVE_ARGS_CONVERTER = (method, o) -> { + private static final BiFunction 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 (value != null && parameterType != null) { + if (!parameterType.isAssignableFrom(value.getClass()) + && ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) { + + convertedArg = ReactiveWrapperConverters.toWrapper(value, parameterType); + } } - if (!parameterType.isAssignableFrom(value.getClass()) - && ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) { - - converted[i] = ReactiveWrapperConverters.toWrapper(value, parameterType); - } else { - converted[i] = value; - } + 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, public class QueryExecutorMethodInterceptor implements MethodInterceptor { private final Map queries; + private final Map invocationMetadataCache = new ConcurrentReferenceHashMap<>(); private final QueryExecutionResultHandler resultHandler; /** @@ -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, 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 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); + } + } } 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 a83a2aaa6..7e7e61cbc 100644 --- a/src/main/java/org/springframework/data/repository/query/Parameter.java +++ b/src/main/java/org/springframework/data/repository/query/Parameter.java @@ -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; 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; */ public class Parameter { - static final List> TYPES = Arrays.asList(Pageable.class, Sort.class); + static final List> TYPES; private static final String NAMED_PARAMETER_TEMPLATE = ":%s"; private static final String POSITION_PARAMETER_TEMPLATE = "?%s"; @@ -51,6 +54,21 @@ public class Parameter { private final boolean isDynamicProjectionParameter; private final Lazy> name; + static { + + List> 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 { 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()); }); diff --git a/src/main/java/org/springframework/data/repository/reactive/package-info.java b/src/main/java/org/springframework/data/repository/reactive/package-info.java index 8b110b5dc..26c672284 100644 --- a/src/main/java/org/springframework/data/repository/reactive/package-info.java +++ b/src/main/java/org/springframework/data/repository/reactive/package-info.java @@ -1,5 +1,5 @@ /** - * Support for reactive repository. + * Support for reactive repositories. */ @org.springframework.lang.NonNullApi package org.springframework.data.repository.reactive; diff --git a/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java b/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java index fdd8c6498..da1b22569 100644 --- a/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java +++ b/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java @@ -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; 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 { 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 { } } + // ------------------------------------------------------------------------- + // Coroutine converters + // ------------------------------------------------------------------------- + + /** + * A {@link Converter} to convert a {@link Publisher} to {@link Flow}. + * + * @author Mark Paluch + * @author 2.3 + */ + private enum PublisherToFlowConverter implements Converter, Flow> { + + INSTANCE; + + @Nonnull + @Override + public Flow convert(Publisher source) { + return ReactiveFlowKt.asFlow(source); + } + } + // ------------------------------------------------------------------------- // RxJava 1 converters // ------------------------------------------------------------------------- @@ -1064,6 +1097,38 @@ public class ReactiveWrapperConverters { } } + /** + * A {@link ConverterFactory} that adapts between reactive types using {@link ReactiveAdapterRegistry}. + */ + private enum ReactiveAdapterConverterFactory implements ConverterFactory, 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 Converter getConverter(Class 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}. * diff --git a/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java b/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java index d9f090d12..c2e5c06e8 100644 --- a/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java +++ b/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java @@ -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; 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 { 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 REACTIVE_WRAPPERS; /** @@ -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 { 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) (flowCollector, continuation) -> Unit.INSTANCE; + })); + } } REACTIVE_WRAPPERS = Collections.unmodifiableCollection(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)); } diff --git a/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java new file mode 100644 index 000000000..b4704eb34 --- /dev/null +++ b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java @@ -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> 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> 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); + } +} diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index d5dcfc6c2..f4a83ad7b 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -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 { 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> 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> 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); - } - } } diff --git a/src/main/kotlin/org/springframework/data/repository/kotlin/CoCrudRepository.kt b/src/main/kotlin/org/springframework/data/repository/kotlin/CoCrudRepository.kt new file mode 100644 index 000000000..4b3b08d38 --- /dev/null +++ b/src/main/kotlin/org/springframework/data/repository/kotlin/CoCrudRepository.kt @@ -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 : Repository { + + /** + * 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 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 saveAll(entities: Iterable): Flow + + /** + * 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 saveAll(entityStream: Flow): Flow + + /** + * 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 + + /** + * 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): Flow + + /** + * 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) + + /** + * Deletes all entities managed by the repository. + */ + suspend fun deleteAll() +} diff --git a/src/main/kotlin/org/springframework/data/repository/kotlin/CoSortingRepository.kt b/src/main/kotlin/org/springframework/data/repository/kotlin/CoSortingRepository.kt new file mode 100644 index 000000000..e7476b9bb --- /dev/null +++ b/src/main/kotlin/org/springframework/data/repository/kotlin/CoSortingRepository.kt @@ -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 : CoCrudRepository { + + /** + * 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 +} diff --git a/src/main/kotlin/org/springframework/data/repository/kotlin/package-info.java b/src/main/kotlin/org/springframework/data/repository/kotlin/package-info.java new file mode 100644 index 000000000..a8b71d97a --- /dev/null +++ b/src/main/kotlin/org/springframework/data/repository/kotlin/package-info.java @@ -0,0 +1,5 @@ +/** + * Support for Kotlin Coroutines repositories. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.repository.kotlin; diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyReactiveRepositoryFactory.java b/src/test/java/org/springframework/data/repository/core/support/DummyReactiveRepositoryFactory.java new file mode 100644 index 000000000..162d648e0 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/DummyReactiveRepositoryFactory.java @@ -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 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 EntityInformation getEntityInformation(Class 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 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; + } +} diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java index 44b685eeb..36b64eeb0 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java @@ -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 { ? fragments.append(RepositoryFragments.just(querydsl)) // : fragments; } + + /** + * @author Mark Paluch + */ + public static interface MyRepositoryQuery extends RepositoryQuery { + + } } diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index 086689cd1..c72d0498e 100755 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -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 { 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 { 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 { 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 { } - interface MyRepositoryQuery extends RepositoryQuery { - - } - interface ReadOnlyRepository extends Repository { Optional findById(ID id); diff --git a/src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryCustomImplementationUnitTests.kt b/src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryCustomImplementationUnitTests.kt new file mode 100644 index 000000000..1e10928a6 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryCustomImplementationUnitTests.kt @@ -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>() + 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, MyCustomCoRepository + + interface MyCustomCoRepository { + + suspend fun findOne(id: String): User + } + + class MyCustomCoRepositoryImpl : MyCustomCoRepository { + + override suspend fun findOne(id: String): User { + return User() + } + } +} diff --git a/src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryUnitTests.kt b/src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryUnitTests.kt new file mode 100644 index 000000000..905c99fbf --- /dev/null +++ b/src/test/kotlin/org/springframework/data/repository/kotlin/CoCrudRepositoryUnitTests.kt @@ -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>() + 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()) + + 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()) + + 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 { + + suspend fun findOne(id: String): User + + fun findMultiple(id: String): Flow + + suspend fun findSuspendedMultiple(id: String): Flow + } +} diff --git a/src/test/kotlin/org/springframework/data/repository/query/ParameterUnitTests.kt b/src/test/kotlin/org/springframework/data/repository/query/ParameterUnitTests.kt new file mode 100644 index 000000000..298053ee0 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/repository/query/ParameterUnitTests.kt @@ -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 + } +}