DATACMNS-983 - Added support for Javaslang and Vavr Try as method return types.
We now support the use of Vavr's Try as repository method return type so that exceptions caused by the repository method execution are wrapped into a Failure. Introduced a return type specific indirection of the execution. We now also recursively handle wrapper types so that Try<Option<…>> is valid, too. Added support for Javaslang's Try, too.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<WrapperType> UNWRAPPER_TYPES = new HashSet<WrapperType>();
|
||||
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<Converter<Object, Object>>();
|
||||
private static final Set<Class<?>> ALLOWED_PAGEABLE_TYPES = new HashSet<Class<?>>();
|
||||
private static final Map<Class<?>, 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.
|
||||
|
||||
@@ -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> 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<Entity> single();
|
||||
|
||||
Completable completable();
|
||||
|
||||
// DATACMNS-1056
|
||||
Option<Entity> option();
|
||||
|
||||
// DATACMNS-1165
|
||||
Streamable<Entity> streamable();
|
||||
|
||||
// DATACMNS-938
|
||||
Try<Option<Entity>> tryOfOption();
|
||||
}
|
||||
|
||||
static class Entity {}
|
||||
|
||||
@@ -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<String> pages();
|
||||
|
||||
Try<Sample> tryMethod();
|
||||
|
||||
Try<Seq<Sample>> tryForSeqMethod();
|
||||
|
||||
io.vavr.control.Try<Sample> vavrTryMethod();
|
||||
|
||||
io.vavr.control.Try<io.vavr.collection.Seq<Sample>> vavrTryForSeqMethod();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user