Skip to content

Commit 0f9e88f

Browse files
committed
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.
1 parent 7ab2c84 commit 0f9e88f

File tree

5 files changed

+129
-9
lines changed

5 files changed

+129
-9
lines changed

src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public QueryExecutionResultHandler() {
4848

4949
GenericConversionService conversionService = new DefaultConversionService();
5050
QueryExecutionConverters.registerConvertersIn(conversionService);
51+
conversionService.removeConvertible(Object.class, Object.class);
5152

5253
this.conversionService = conversionService;
5354
}
@@ -67,9 +68,9 @@ public Object postProcessInvocationResult(@Nullable Object result, Method method
6768
}
6869

6970
MethodParameter parameter = new MethodParameter(method, -1);
70-
TypeDescriptor methodReturnTypeDescriptor = TypeDescriptor.nested(parameter, 0);
7171

72-
return postProcessInvocationResult(result, methodReturnTypeDescriptor);
72+
return postProcessInvocationResult(result, 0, parameter);
73+
7374
}
7475

7576
/**
@@ -80,7 +81,9 @@ public Object postProcessInvocationResult(@Nullable Object result, Method method
8081
* @return
8182
*/
8283
@Nullable
83-
Object postProcessInvocationResult(@Nullable Object result, @Nullable TypeDescriptor returnTypeDescriptor) {
84+
Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, MethodParameter parameter) {
85+
86+
TypeDescriptor returnTypeDescriptor = TypeDescriptor.nested(parameter, nestingLevel);
8487

8588
if (returnTypeDescriptor == null) {
8689
return result;
@@ -104,6 +107,9 @@ Object postProcessInvocationResult(@Nullable Object result, @Nullable TypeDescri
104107

105108
if (QueryExecutionConverters.supports(expectedReturnType)) {
106109

110+
// For a wrapper type, try nested resolution first
111+
result = postProcessInvocationResult(result, nestingLevel + 1, parameter);
112+
107113
TypeDescriptor targetType = TypeDescriptor.valueOf(expectedReturnType);
108114

109115
if (conversionService.canConvert(WRAPPER_TYPE, returnTypeDescriptor)

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
5858
import org.springframework.data.repository.query.RepositoryQuery;
5959
import org.springframework.data.repository.util.ClassUtils;
60+
import org.springframework.data.repository.util.QueryExecutionConverters;
6061
import org.springframework.data.repository.util.ReactiveWrapperConverters;
6162
import org.springframework.data.repository.util.ReactiveWrappers;
6263
import org.springframework.data.util.Pair;
@@ -575,9 +576,11 @@ private void invokeListeners(RepositoryQuery query) {
575576
@Nullable
576577
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
577578

578-
Object result = doInvoke(invocation);
579+
Method method = invocation.getMethod();
579580

580-
return resultHandler.postProcessInvocationResult(result, invocation.getMethod());
581+
return QueryExecutionConverters //
582+
.getExecutionAdapter(method.getReturnType()) //
583+
.apply(() -> resultHandler.postProcessInvocationResult(doInvoke(invocation), method));
581584
}
582585

583586
@Nullable

src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import javaslang.collection.Seq;
1919
import javaslang.collection.Traversable;
20+
import javaslang.control.Try;
2021
import lombok.AccessLevel;
2122
import lombok.Getter;
2223
import lombok.NonNull;
@@ -29,8 +30,10 @@
2930
import java.util.Arrays;
3031
import java.util.Collection;
3132
import java.util.Collections;
33+
import java.util.HashMap;
3234
import java.util.HashSet;
3335
import java.util.List;
36+
import java.util.Map;
3437
import java.util.Set;
3538
import java.util.concurrent.CompletableFuture;
3639
import java.util.concurrent.Future;
@@ -100,6 +103,7 @@ public abstract class QueryExecutionConverters {
100103
private static final Set<WrapperType> UNWRAPPER_TYPES = new HashSet<WrapperType>();
101104
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<Converter<Object, Object>>();
102105
private static final Set<Class<?>> ALLOWED_PAGEABLE_TYPES = new HashSet<Class<?>>();
106+
private static final Map<Class<?>, ExecutionAdapter> EXECUTION_ADAPTER = new HashMap<>();
103107

104108
static {
105109

@@ -142,6 +146,10 @@ public abstract class QueryExecutionConverters {
142146

143147
UNWRAPPERS.add(JavaslangOptionUnwrapper.INSTANCE);
144148

149+
// Try support
150+
WRAPPER_TYPES.add(WrapperType.singleValue(Try.class));
151+
EXECUTION_ADAPTER.put(Try.class, it -> Try.of(it::get));
152+
145153
ALLOWED_PAGEABLE_TYPES.add(Seq.class);
146154
}
147155

@@ -152,6 +160,10 @@ public abstract class QueryExecutionConverters {
152160

153161
UNWRAPPERS.add(VavrOptionUnwrapper.INSTANCE);
154162

163+
// Try support
164+
WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class));
165+
EXECUTION_ADAPTER.put(io.vavr.control.Try.class, it -> io.vavr.control.Try.of(it::get));
166+
155167
ALLOWED_PAGEABLE_TYPES.add(io.vavr.collection.Seq.class);
156168
}
157169

@@ -311,6 +323,27 @@ public static TypeInformation<?> unwrapWrapperTypes(TypeInformation<?> type) {
311323
return needToUnwrap ? unwrapWrapperTypes(type.getRequiredComponentType()) : type;
312324
}
313325

326+
/**
327+
* Returns the {@link ExecutionAdapter} to be used for the given return type.
328+
*
329+
* @param returnType must not be {@literal null}.
330+
* @return
331+
*/
332+
public static ExecutionAdapter getExecutionAdapter(Class<?> returnType) {
333+
334+
Assert.notNull(returnType, "Return type must not be null!");
335+
336+
return EXECUTION_ADAPTER.getOrDefault(returnType, ThrowingSupplier::get);
337+
}
338+
339+
public interface ThrowingSupplier {
340+
Object get() throws Throwable;
341+
}
342+
343+
public interface ExecutionAdapter {
344+
Object apply(ThrowingSupplier supplier) throws Throwable;
345+
}
346+
314347
/**
315348
* Base class for converters that create instances of wrapper types such as Google Guava's and JDK 8's
316349
* {@code Optional} types.

src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import io.vavr.control.Try;
2021
import javaslang.control.Option;
2122
import reactor.core.publisher.Flux;
2223
import reactor.core.publisher.Mono;
@@ -35,7 +36,6 @@
3536

3637
import org.junit.Test;
3738
import org.reactivestreams.Publisher;
38-
import org.springframework.core.convert.TypeDescriptor;
3939
import org.springframework.dao.InvalidDataAccessApiUsageException;
4040
import org.springframework.data.repository.Repository;
4141
import org.springframework.data.util.Streamable;
@@ -345,23 +345,33 @@ public void convertsOptionalToThirdPartyOption() throws Exception {
345345
Entity value = new Entity();
346346
Optional<Entity> entity = Optional.of(value);
347347

348-
Object result = handler.postProcessInvocationResult(entity, TypeDescriptor.valueOf(Option.class));
348+
Object result = handler.postProcessInvocationResult(entity, getMethod("option"));
349349

350350
assertThat(result).isInstanceOfSatisfying(Option.class, it -> assertThat(it.get()).isEqualTo(value));
351351
}
352352

353353
@Test // DATACMNS-1165
354354
@SuppressWarnings("unchecked")
355-
public void convertsIterableIntoStreamable() {
355+
public void convertsIterableIntoStreamable() throws Exception {
356356

357357
Iterable<?> source = Arrays.asList(new Object());
358358

359-
Object result = handler.postProcessInvocationResult(source, TypeDescriptor.valueOf(Streamable.class));
359+
Object result = handler.postProcessInvocationResult(source, getMethod("streamable"));
360360

361361
assertThat(result).isInstanceOfSatisfying(Streamable.class,
362362
it -> assertThat(it.stream().collect(Collectors.toList())).isEqualTo(source));
363363
}
364364

365+
@Test // DATACMNS-938
366+
public void resolvesNestedWrapperIfOuterDoesntNeedConversion() throws Exception {
367+
368+
Entity entity = new Entity();
369+
370+
Object result = handler.postProcessInvocationResult(entity, getMethod("tryOfOption"));
371+
372+
assertThat(result).isInstanceOfSatisfying(Option.class, it -> assertThat(it.get()).isEqualTo(entity));
373+
}
374+
365375
private static Method getMethod(String methodName) throws Exception {
366376
return Sample.class.getMethod(methodName);
367377
}
@@ -387,6 +397,15 @@ static interface Sample extends Repository<Entity, Long> {
387397
Single<Entity> single();
388398

389399
Completable completable();
400+
401+
// DATACMNS-1056
402+
Option<Entity> option();
403+
404+
// DATACMNS-1165
405+
Streamable<Entity> streamable();
406+
407+
// DATACMNS-938
408+
Try<Option<Entity>> tryOfOption();
390409
}
391410

392411
static class Entity {}

src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
import javaslang.collection.LinkedHashSet;
2323
import javaslang.collection.Seq;
2424
import javaslang.collection.Traversable;
25+
import javaslang.control.Try;
26+
import javaslang.control.Try.Failure;
2527
import reactor.core.publisher.Flux;
2628
import reactor.core.publisher.Mono;
2729
import rx.Completable;
2830
import rx.Observable;
2931
import rx.Single;
3032
import scala.Option;
3133

34+
import java.io.IOException;
3235
import java.lang.reflect.Method;
3336
import java.util.Arrays;
3437
import java.util.Collections;
@@ -361,8 +364,64 @@ public void unwrapsPages() throws Exception {
361364
.isEqualTo(ClassTypeInformation.from(String.class));
362365
}
363366

367+
@Test // DATACMNS-983
368+
public void exposesExecutionAdapterForJavaslangTry() throws Throwable {
369+
370+
Object result = getExecutionAdapter(Try.class).apply(() -> {
371+
throw new IOException("Some message!");
372+
});
373+
374+
assertThat(result).isInstanceOf(Failure.class);
375+
}
376+
377+
@Test // DATACMNS-983
378+
public void unwrapsDomainTypeFromJavaslangTryWrapper() throws Exception {
379+
380+
for (String methodName : Arrays.asList("tryMethod", "tryForSeqMethod")) {
381+
382+
Method method = Sample.class.getMethod(methodName);
383+
384+
TypeInformation<?> type = QueryExecutionConverters
385+
.unwrapWrapperTypes(ClassTypeInformation.fromReturnTypeOf(method));
386+
387+
assertThat(type.getType()).isEqualTo(Sample.class);
388+
}
389+
}
390+
391+
@Test // DATACMNS-983
392+
public void exposesExecutionAdapterForVavrTry() throws Throwable {
393+
394+
Object result = getExecutionAdapter(io.vavr.control.Try.class).apply(() -> {
395+
throw new IOException("Some message!");
396+
});
397+
398+
assertThat(result).isInstanceOf(io.vavr.control.Try.Failure.class);
399+
}
400+
401+
@Test // DATACMNS-983
402+
public void unwrapsDomainTypeFromVavrTryWrapper() throws Exception {
403+
404+
for (String methodName : Arrays.asList("tryMethod", "tryForSeqMethod")) {
405+
406+
Method method = Sample.class.getMethod(methodName);
407+
408+
TypeInformation<?> type = QueryExecutionConverters
409+
.unwrapWrapperTypes(ClassTypeInformation.fromReturnTypeOf(method));
410+
411+
assertThat(type.getType()).isEqualTo(Sample.class);
412+
}
413+
}
414+
364415
interface Sample {
365416

366417
Page<String> pages();
418+
419+
Try<Sample> tryMethod();
420+
421+
Try<Seq<Sample>> tryForSeqMethod();
422+
423+
io.vavr.control.Try<Sample> vavrTryMethod();
424+
425+
io.vavr.control.Try<io.vavr.collection.Seq<Sample>> vavrTryForSeqMethod();
367426
}
368427
}

0 commit comments

Comments
 (0)