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 47fa66b26..cbd23a3c6 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,9 +45,11 @@ import javax.annotation.Nonnull; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Slice; import org.springframework.data.geo.GeoResults; @@ -273,7 +276,7 @@ public abstract class QueryExecutionConverters { } conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); - conversionService.addConverter(IterableToStreamableConverter.INSTANCE); + conversionService.addConverter(new IterableToStreamableConverter()); } /** @@ -757,18 +760,60 @@ public abstract class QueryExecutionConverters { } } - private enum IterableToStreamableConverter implements Converter, Streamable> { + @RequiredArgsConstructor + private static class IterableToStreamableConverter implements ConditionalGenericConverter { - INSTANCE; + private static final TypeDescriptor STREAMABLE = TypeDescriptor.valueOf(Streamable.class); - /* + private final Map TARGET_TYPE_CACHE = new ConcurrentHashMap<>(); + private final ConversionService conversionService = DefaultConversionService.getSharedInstance(); + + /* * (non-Javadoc) - * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() */ - @Nonnull + @org.springframework.lang.NonNull @Override - public Streamable convert(Iterable source) { - return Streamable.of(source); + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Iterable.class, Object.class)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) + */ + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (!Iterable.class.isAssignableFrom(sourceType.getType())) { + return false; + } + + if (Streamable.class.equals(targetType.getType())) { + return true; + } + + return TARGET_TYPE_CACHE.computeIfAbsent(targetType, it -> { + return conversionService.canConvert(STREAMABLE, targetType); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) + */ + @SuppressWarnings("unchecked") + @Nullable + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + + Streamable streamable = source == null // + ? Streamable.empty() // + : Streamable.of(Iterable.class.cast(source)); + + return Streamable.class.equals(targetType.getType()) // + ? streamable // + : conversionService.convert(streamable, STREAMABLE, targetType); } } 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 a69d6560d..af94e2d6c 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 @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; import io.vavr.control.Try; import javaslang.control.Option; +import lombok.Value; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Completable; @@ -28,6 +29,7 @@ import rx.Single; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -372,6 +374,16 @@ public class QueryExecutionResultHandlerUnitTests { assertThat(result).isInstanceOfSatisfying(Option.class, it -> assertThat(it.get()).isEqualTo(entity)); } + @Test // DATACMNS-1430 + public void convertsElementsAndValueIntoCustomStreamable() throws Exception { + + Object result = handler.postProcessInvocationResult(Arrays.asList("foo"), getMethod("customStreamable")); + + assertThat(result).isInstanceOfSatisfying(CustomStreamableWrapper.class, it -> { + assertThat(it).containsExactly("foo"); + }); + } + private static Method getMethod(String methodName) throws Exception { return Sample.class.getMethod(methodName); } @@ -406,7 +418,23 @@ public class QueryExecutionResultHandlerUnitTests { // DATACMNS-938 Try> tryOfOption(); + + // DATACMNS-1430 + CustomStreamableWrapper customStreamable(); } static class Entity {} + + // DATACMNS-1430 + + @Value + static class CustomStreamableWrapper implements Streamable { + + Streamable source; + + @Override + public Iterator iterator() { + return source.iterator(); + } + } } 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 7ee9de7cb..2ea8e61ae 100755 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -24,6 +24,7 @@ import javaslang.collection.Seq; import javaslang.collection.Traversable; import javaslang.control.Try; import javaslang.control.Try.Failure; +import lombok.Value; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Completable; @@ -35,6 +36,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -48,6 +50,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Slice; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; import org.springframework.util.concurrent.ListenableFuture; @@ -412,6 +415,29 @@ public class QueryExecutionConvertersUnitTests { } } + @Test // DATACMNS-1430 + public void returnsStreamableForIterable() throws Exception { + + assertThat(conversionService.canConvert(Iterable.class, Streamable.class)).isTrue(); + assertThat(conversionService.convert(Arrays.asList("foo"), Streamable.class)).containsExactly("foo"); + } + + @Test // DATACMNS-1430 + public void convertsToStreamableWrapper() throws Exception { + + assertThat(conversionService.canConvert(Iterable.class, StreamableWrapper.class)).isTrue(); + assertThat(conversionService.convert(Arrays.asList("foo"), StreamableWrapper.class).getStreamable()) // + .containsExactly("foo"); + } + + @Test // DATACMNS-1430 + public void convertsToStreamableWrapperImplementingStreamable() throws Exception { + + assertThat(conversionService.canConvert(Iterable.class, CustomStreamableWrapper.class)).isTrue(); + assertThat(conversionService.convert(Arrays.asList("foo"), CustomStreamableWrapper.class)) // + .containsExactly("foo"); + } + interface Sample { Page pages(); @@ -424,4 +450,22 @@ public class QueryExecutionConvertersUnitTests { io.vavr.control.Try> vavrTryForSeqMethod(); } + + // DATACMNS-1430 + + @Value(staticConstructor = "of") + static class StreamableWrapper { + Streamable streamable; + } + + @Value + static class CustomStreamableWrapper implements Streamable { + + Streamable source; + + @Override + public Iterator iterator() { + return source.iterator(); + } + } }