diff --git a/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java b/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java index c0f38e352..bb4a97f86 100644 --- a/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java +++ b/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java @@ -48,6 +48,7 @@ class QueryExecutionResultHandler { GenericConversionService conversionService = new DefaultConversionService(); QueryExecutionConverters.registerConvertersIn(conversionService); + conversionService.removeConvertible(Object.class, Object.class); this.conversionService = conversionService; } @@ -67,9 +68,9 @@ class QueryExecutionResultHandler { } MethodParameter parameter = new MethodParameter(method, -1); - TypeDescriptor methodReturnTypeDescriptor = TypeDescriptor.nested(parameter, 0); - return postProcessInvocationResult(result, methodReturnTypeDescriptor); + return postProcessInvocationResult(result, 0, parameter); + } /** @@ -80,7 +81,9 @@ class QueryExecutionResultHandler { * @return */ @Nullable - Object postProcessInvocationResult(@Nullable Object result, @Nullable TypeDescriptor returnTypeDescriptor) { + Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, MethodParameter parameter) { + + TypeDescriptor returnTypeDescriptor = TypeDescriptor.nested(parameter, nestingLevel); if (returnTypeDescriptor == null) { return result; @@ -104,6 +107,9 @@ class QueryExecutionResultHandler { if (QueryExecutionConverters.supports(expectedReturnType)) { + // For a wrapper type, try nested resolution first + result = postProcessInvocationResult(result, nestingLevel + 1, parameter); + TypeDescriptor targetType = TypeDescriptor.valueOf(expectedReturnType); if (conversionService.canConvert(WRAPPER_TYPE, returnTypeDescriptor) 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 297239c8d..b1dda83e0 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 @@ -57,6 +57,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; 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.Pair; @@ -575,9 +576,11 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, @Nullable public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable { - Object result = doInvoke(invocation); + Method method = invocation.getMethod(); - return resultHandler.postProcessInvocationResult(result, invocation.getMethod()); + return QueryExecutionConverters // + .getExecutionAdapter(method.getReturnType()) // + .apply(() -> resultHandler.postProcessInvocationResult(doInvoke(invocation), method)); } @Nullable diff --git a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java index 059359854..47fa66b26 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -17,6 +17,7 @@ package org.springframework.data.repository.util; import javaslang.collection.Seq; import javaslang.collection.Traversable; +import javaslang.control.Try; import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; @@ -29,8 +30,10 @@ import scala.runtime.AbstractFunction0; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -100,6 +103,7 @@ public abstract class QueryExecutionConverters { private static final Set UNWRAPPER_TYPES = new HashSet(); private static final Set> UNWRAPPERS = new HashSet>(); private static final Set> ALLOWED_PAGEABLE_TYPES = new HashSet>(); + private static final Map, ExecutionAdapter> EXECUTION_ADAPTER = new HashMap<>(); static { @@ -142,6 +146,10 @@ public abstract class QueryExecutionConverters { UNWRAPPERS.add(JavaslangOptionUnwrapper.INSTANCE); + // Try support + WRAPPER_TYPES.add(WrapperType.singleValue(Try.class)); + EXECUTION_ADAPTER.put(Try.class, it -> Try.of(it::get)); + ALLOWED_PAGEABLE_TYPES.add(Seq.class); } @@ -152,6 +160,10 @@ public abstract class QueryExecutionConverters { UNWRAPPERS.add(VavrOptionUnwrapper.INSTANCE); + // Try support + WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class)); + EXECUTION_ADAPTER.put(io.vavr.control.Try.class, it -> io.vavr.control.Try.of(it::get)); + ALLOWED_PAGEABLE_TYPES.add(io.vavr.collection.Seq.class); } @@ -311,6 +323,27 @@ public abstract class QueryExecutionConverters { return needToUnwrap ? unwrapWrapperTypes(type.getRequiredComponentType()) : type; } + /** + * Returns the {@link ExecutionAdapter} to be used for the given return type. + * + * @param returnType must not be {@literal null}. + * @return + */ + public static ExecutionAdapter getExecutionAdapter(Class returnType) { + + Assert.notNull(returnType, "Return type must not be null!"); + + return EXECUTION_ADAPTER.getOrDefault(returnType, ThrowingSupplier::get); + } + + public interface ThrowingSupplier { + Object get() throws Throwable; + } + + public interface ExecutionAdapter { + Object apply(ThrowingSupplier supplier) throws Throwable; + } + /** * Base class for converters that create instances of wrapper types such as Google Guava's and JDK 8's * {@code Optional} types. diff --git a/src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java index 63699b469..a69d6560d 100755 --- a/src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.repository.core.support; import static org.assertj.core.api.Assertions.*; +import io.vavr.control.Try; import javaslang.control.Option; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -35,7 +36,6 @@ import java.util.stream.Collectors; import org.junit.Test; import org.reactivestreams.Publisher; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.repository.Repository; import org.springframework.data.util.Streamable; @@ -345,23 +345,33 @@ public class QueryExecutionResultHandlerUnitTests { Entity value = new Entity(); Optional entity = Optional.of(value); - Object result = handler.postProcessInvocationResult(entity, TypeDescriptor.valueOf(Option.class)); + Object result = handler.postProcessInvocationResult(entity, getMethod("option")); assertThat(result).isInstanceOfSatisfying(Option.class, it -> assertThat(it.get()).isEqualTo(value)); } @Test // DATACMNS-1165 @SuppressWarnings("unchecked") - public void convertsIterableIntoStreamable() { + public void convertsIterableIntoStreamable() throws Exception { Iterable source = Arrays.asList(new Object()); - Object result = handler.postProcessInvocationResult(source, TypeDescriptor.valueOf(Streamable.class)); + Object result = handler.postProcessInvocationResult(source, getMethod("streamable")); assertThat(result).isInstanceOfSatisfying(Streamable.class, it -> assertThat(it.stream().collect(Collectors.toList())).isEqualTo(source)); } + @Test // DATACMNS-938 + public void resolvesNestedWrapperIfOuterDoesntNeedConversion() throws Exception { + + Entity entity = new Entity(); + + Object result = handler.postProcessInvocationResult(entity, getMethod("tryOfOption")); + + assertThat(result).isInstanceOfSatisfying(Option.class, it -> assertThat(it.get()).isEqualTo(entity)); + } + private static Method getMethod(String methodName) throws Exception { return Sample.class.getMethod(methodName); } @@ -387,6 +397,15 @@ public class QueryExecutionResultHandlerUnitTests { Single single(); Completable completable(); + + // DATACMNS-1056 + Option option(); + + // DATACMNS-1165 + Streamable streamable(); + + // DATACMNS-938 + Try> tryOfOption(); } static class Entity {} diff --git a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java index b3bc69624..7ee9de7cb 100755 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -22,6 +22,8 @@ import javaslang.collection.LinkedHashMap; import javaslang.collection.LinkedHashSet; import javaslang.collection.Seq; import javaslang.collection.Traversable; +import javaslang.control.Try; +import javaslang.control.Try.Failure; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Completable; @@ -29,6 +31,7 @@ import rx.Observable; import rx.Single; import scala.Option; +import java.io.IOException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; @@ -361,8 +364,64 @@ public class QueryExecutionConvertersUnitTests { .isEqualTo(ClassTypeInformation.from(String.class)); } + @Test // DATACMNS-983 + public void exposesExecutionAdapterForJavaslangTry() throws Throwable { + + Object result = getExecutionAdapter(Try.class).apply(() -> { + throw new IOException("Some message!"); + }); + + assertThat(result).isInstanceOf(Failure.class); + } + + @Test // DATACMNS-983 + public void unwrapsDomainTypeFromJavaslangTryWrapper() throws Exception { + + for (String methodName : Arrays.asList("tryMethod", "tryForSeqMethod")) { + + Method method = Sample.class.getMethod(methodName); + + TypeInformation type = QueryExecutionConverters + .unwrapWrapperTypes(ClassTypeInformation.fromReturnTypeOf(method)); + + assertThat(type.getType()).isEqualTo(Sample.class); + } + } + + @Test // DATACMNS-983 + public void exposesExecutionAdapterForVavrTry() throws Throwable { + + Object result = getExecutionAdapter(io.vavr.control.Try.class).apply(() -> { + throw new IOException("Some message!"); + }); + + assertThat(result).isInstanceOf(io.vavr.control.Try.Failure.class); + } + + @Test // DATACMNS-983 + public void unwrapsDomainTypeFromVavrTryWrapper() throws Exception { + + for (String methodName : Arrays.asList("tryMethod", "tryForSeqMethod")) { + + Method method = Sample.class.getMethod(methodName); + + TypeInformation type = QueryExecutionConverters + .unwrapWrapperTypes(ClassTypeInformation.fromReturnTypeOf(method)); + + assertThat(type.getType()).isEqualTo(Sample.class); + } + } + interface Sample { Page pages(); + + Try tryMethod(); + + Try> tryForSeqMethod(); + + io.vavr.control.Try vavrTryMethod(); + + io.vavr.control.Try> vavrTryForSeqMethod(); } }