1 /*
2  * Copyright 2014-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.repository.util;
17
18 import lombok.AccessLevel;
19 import lombok.Getter;
20 import lombok.NonNull;
21 import lombok.RequiredArgsConstructor;
22 import lombok.Value;
23 import scala.Function0;
24 import scala.Option;
25 import scala.runtime.AbstractFunction0;
26
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.concurrent.CompletableFuture;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.concurrent.Future;
38 import java.util.stream.Stream;
39
40 import javax.annotation.Nonnull;
41
42 import org.springframework.core.convert.ConversionService;
43 import org.springframework.core.convert.TypeDescriptor;
44 import org.springframework.core.convert.converter.ConditionalGenericConverter;
45 import org.springframework.core.convert.converter.Converter;
46 import org.springframework.core.convert.converter.GenericConverter;
47 import org.springframework.core.convert.support.ConfigurableConversionService;
48 import org.springframework.core.convert.support.DefaultConversionService;
49 import org.springframework.data.domain.Page;
50 import org.springframework.data.domain.Slice;
51 import org.springframework.data.geo.GeoResults;
52 import org.springframework.data.util.StreamUtils;
53 import org.springframework.data.util.Streamable;
54 import org.springframework.data.util.TypeInformation;
55 import org.springframework.lang.Nullable;
56 import org.springframework.scheduling.annotation.AsyncResult;
57 import org.springframework.util.Assert;
58 import org.springframework.util.ClassUtils;
59 import org.springframework.util.ConcurrentReferenceHashMap;
60 import org.springframework.util.concurrent.ListenableFuture;
61
62 import com.google.common.base.Optional;
63
64 /**
65  * Converters to potentially wrap the execution of a repository method into a variety of wrapper types potentially being
66  * available on the classpath. Currently supported:
67  * <ul>
68  * <li>{@code java.util.Optional}</li>
69  * <li>{@code com.google.common.base.Optional}</li>
70  * <li>{@code scala.Option} - as of 1.12</li>
71  * <li>{@code java.util.concurrent.Future}</li>
72  * <li>{@code java.util.concurrent.CompletableFuture}</li>
73  * <li>{@code org.springframework.util.concurrent.ListenableFuture<}</li>
74  * <li>{@code javaslang.control.Option} - as of 1.13</li>
75  * <li>{@code javaslang.collection.Seq}, {@code javaslang.collection.Map}, {@code javaslang.collection.Set} - as of
76  * 1.13</li>
77  * <li>{@code io.vavr.collection.Seq}, {@code io.vavr.collection.Map}, {@code io.vavr.collection.Set} - as of 2.0</li>
78  * <li>Reactive wrappers supported by {@link ReactiveWrappers} - as of 2.0</li>
79  * </ul>
80  *
81  * @author Oliver Gierke
82  * @author Mark Paluch
83  * @author Christoph Strobl
84  * @author Maciek Opała
85  * @author Jens Schauder
86  * @since 1.8
87  * @see ReactiveWrappers
88  */

89 public abstract class QueryExecutionConverters {
90
91     private static final boolean GUAVA_PRESENT = ClassUtils.isPresent("com.google.common.base.Optional",
92             QueryExecutionConverters.class.getClassLoader());
93     private static final boolean JDK_8_PRESENT = ClassUtils.isPresent("java.util.Optional",
94             QueryExecutionConverters.class.getClassLoader());
95     private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option",
96             QueryExecutionConverters.class.getClassLoader());
97     private static final boolean VAVR_PRESENT = ClassUtils.isPresent("io.vavr.control.Option",
98             QueryExecutionConverters.class.getClassLoader());
99
100     private static final Set<WrapperType> WRAPPER_TYPES = new HashSet<WrapperType>();
101     private static final Set<WrapperType> UNWRAPPER_TYPES = new HashSet<WrapperType>();
102     private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<Converter<Object, Object>>();
103     private static final Set<Class<?>> ALLOWED_PAGEABLE_TYPES = new HashSet<Class<?>>();
104     private static final Map<Class<?>, ExecutionAdapter> EXECUTION_ADAPTER = new HashMap<>();
105     private static final Map<Class<?>, Boolean> SUPPORTS_CACHE = new ConcurrentReferenceHashMap<>();
106
107     static {
108
109         WRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
110         UNWRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
111         WRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
112         UNWRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
113
114         ALLOWED_PAGEABLE_TYPES.add(Slice.class);
115         ALLOWED_PAGEABLE_TYPES.add(Page.class);
116         ALLOWED_PAGEABLE_TYPES.add(List.class);
117
118         if (GUAVA_PRESENT) {
119             WRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType());
120             UNWRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType());
121             UNWRAPPERS.add(GuavaOptionalUnwrapper.INSTANCE);
122         }
123
124         if (JDK_8_PRESENT) {
125             WRAPPER_TYPES.add(NullableWrapperToJdk8OptionalConverter.getWrapperType());
126             UNWRAPPER_TYPES.add(NullableWrapperToJdk8OptionalConverter.getWrapperType());
127             UNWRAPPERS.add(Jdk8OptionalUnwrapper.INSTANCE);
128         }
129
130         if (JDK_8_PRESENT) {
131             WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());
132             UNWRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());
133         }
134
135         if (SCALA_PRESENT) {
136             WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType());
137             UNWRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType());
138             UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE);
139         }
140
141         if (VAVR_PRESENT) {
142
143             WRAPPER_TYPES.add(NullableWrapperToVavrOptionConverter.getWrapperType());
144             WRAPPER_TYPES.add(VavrCollections.ToJavaConverter.INSTANCE.getWrapperType());
145
146             UNWRAPPERS.add(VavrOptionUnwrapper.INSTANCE);
147
148             // Try support
149             WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class));
150             EXECUTION_ADAPTER.put(io.vavr.control.Try.class, it -> io.vavr.control.Try.of(it::get));
151
152             ALLOWED_PAGEABLE_TYPES.add(io.vavr.collection.Seq.class);
153         }
154     }
155
156     private QueryExecutionConverters() {}
157
158     /**
159      * Returns whether the given type is a supported wrapper type.
160      *
161      * @param type must not be {@literal null}.
162      * @return
163      */

164     public static boolean supports(Class<?> type) {
165
166         Assert.notNull(type, "Type must not be null!");
167
168         return SUPPORTS_CACHE.computeIfAbsent(type, key -> {
169
170             for (WrapperType candidate : WRAPPER_TYPES) {
171                 if (candidate.getType().isAssignableFrom(key)) {
172                     return true;
173                 }
174             }
175
176             if (ReactiveWrappers.supports(type)) {
177                 return true;
178             }
179
180             return false;
181         });
182     }
183
184     /**
185      * Returns whether the given wrapper type supports unwrapping.
186      *
187      * @param type must not be {@literal null}.
188      * @return
189      */

190     public static boolean supportsUnwrapping(Class<?> type) {
191
192         Assert.notNull(type, "Type must not be null!");
193
194         for (WrapperType candidate : UNWRAPPER_TYPES) {
195             if (candidate.getType().isAssignableFrom(type)) {
196                 return true;
197             }
198         }
199
200         return false;
201     }
202
203     public static boolean isSingleValue(Class<?> type) {
204
205         for (WrapperType candidate : WRAPPER_TYPES) {
206             if (candidate.getType().isAssignableFrom(type)) {
207                 return candidate.isSingleValue();
208             }
209         }
210
211         if (ReactiveWrappers.supports(type) && ReactiveWrappers.isSingleValueType(type)) {
212             return true;
213         }
214
215         return false;
216     }
217
218     /**
219      * Returns the types that are supported on paginating query methods. Will include custom collection types of e.g.
220      * Javaslang.
221      *
222      * @return
223      */

224     public static Set<Class<?>> getAllowedPageableTypes() {
225         return Collections.unmodifiableSet(ALLOWED_PAGEABLE_TYPES);
226     }
227
228     /**
229      * Registers converters for wrapper types found on the classpath.
230      *
231      * @param conversionService must not be {@literal null}.
232      */

233     public static void registerConvertersIn(ConfigurableConversionService conversionService) {
234
235         Assert.notNull(conversionService, "ConversionService must not be null!");
236
237         conversionService.removeConvertible(Collection.class, Object.class);
238
239         if (GUAVA_PRESENT) {
240             conversionService.addConverter(new NullableWrapperToGuavaOptionalConverter(conversionService));
241         }
242
243         if (JDK_8_PRESENT) {
244             conversionService.addConverter(new NullableWrapperToJdk8OptionalConverter(conversionService));
245             conversionService.addConverter(new NullableWrapperToCompletableFutureConverter(conversionService));
246         }
247
248         if (SCALA_PRESENT) {
249             conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService));
250         }
251
252         if (VAVR_PRESENT) {
253             conversionService.addConverter(new NullableWrapperToVavrOptionConverter(conversionService));
254             conversionService.addConverter(VavrCollections.FromJavaConverter.INSTANCE);
255         }
256
257         conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService));
258         conversionService.addConverter(new IterableToStreamableConverter());
259     }
260
261     /**
262      * Unwraps the given source value in case it's one of the currently supported wrapper types detected at runtime.
263      *
264      * @param source can be {@literal null}.
265      * @return
266      */

267     @Nullable
268     public static Object unwrap(@Nullable Object source) {
269
270         if (source == null || !supports(source.getClass())) {
271             return source;
272         }
273
274         for (Converter<Object, Object> converter : UNWRAPPERS) {
275
276             Object result = converter.convert(source);
277
278             if (result != source) {
279                 return result;
280             }
281         }
282
283         return source;
284     }
285
286     /**
287      * Recursively unwraps well known wrapper types from the given {@link TypeInformation}.
288      *
289      * @param type must not be {@literal null}.
290      * @return will never be {@literal null}.
291      */

292     public static TypeInformation<?> unwrapWrapperTypes(TypeInformation<?> type) {
293
294         Assert.notNull(type, "type must not be null");
295
296         Class<?> rawType = type.getType();
297
298         boolean needToUnwrap = type.isCollectionLike() //
299                 || Slice.class.isAssignableFrom(rawType) //
300                 || GeoResults.class.isAssignableFrom(rawType) //
301                 || rawType.isArray() //
302                 || supports(rawType) //
303                 || Stream.class.isAssignableFrom(rawType);
304
305         return needToUnwrap ? unwrapWrapperTypes(type.getRequiredComponentType()) : type;
306     }
307
308     /**
309      * Returns the {@link ExecutionAdapter} to be used for the given return type.
310      *
311      * @param returnType must not be {@literal null}.
312      * @return
313      */

314     @Nullable
315     public static ExecutionAdapter getExecutionAdapter(Class<?> returnType) {
316
317         Assert.notNull(returnType, "Return type must not be null!");
318
319         return EXECUTION_ADAPTER.get(returnType);
320     }
321
322     public interface ThrowingSupplier {
323         Object get() throws Throwable;
324     }
325
326     public interface ExecutionAdapter {
327         Object apply(ThrowingSupplier supplier) throws Throwable;
328     }
329
330     /**
331      * Base class for converters that create instances of wrapper types such as Google Guava's and JDK 8's
332      * {@code Optional} types.
333      *
334      * @author Oliver Gierke
335      */

336     @RequiredArgsConstructor
337     private static abstract class AbstractWrapperTypeConverter implements GenericConverter {
338
339         private final @NonNull ConversionService conversionService;
340         private final @NonNull Object nullValue;
341         private final @NonNull Iterable<Class<?>> wrapperTypes;
342
343         /**
344          * Creates a new {@link AbstractWrapperTypeConverter} using the given {@link ConversionService} and wrapper type.
345          *
346          * @param conversionService must not be {@literal null}.
347          * @param nullValue must not be {@literal null}.
348          */

349         protected AbstractWrapperTypeConverter(ConversionService conversionService, Object nullValue) {
350
351             Assert.notNull(conversionService, "ConversionService must not be null!");
352             Assert.notNull(nullValue, "Null value must not be null!");
353
354             this.conversionService = conversionService;
355             this.nullValue = nullValue;
356             this.wrapperTypes = Collections.singleton(nullValue.getClass());
357         }
358
359         /*
360          * (non-Javadoc)
361          * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
362          */

363         @Nonnull
364         @Override
365         public Set<ConvertiblePair> getConvertibleTypes() {
366
367             return Streamable.of(wrapperTypes)//
368                     .map(it -> new ConvertiblePair(NullableWrapper.class, it))//
369                     .stream().collect(StreamUtils.toUnmodifiableSet());
370         }
371
372         /*
373          * (non-Javadoc)
374          * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
375          */

376         @Nullable
377         @Override
378         public final Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
379
380             if (source == null) {
381                 return null;
382             }
383
384             NullableWrapper wrapper = (NullableWrapper) source;
385             Object value = wrapper.getValue();
386
387             // TODO: Add Recursive conversion once we move to Spring 4
388             return value == null ? nullValue : wrap(value);
389         }
390
391         /**
392          * Wrap the given, non-{@literal null} value into the wrapper type.
393          *
394          * @param source will never be {@literal null}.
395          * @return must not be {@literal null}.
396          */

397         protected abstract Object wrap(Object source);
398     }
399
400     /**
401      * A Spring {@link Converter} to support Google Guava's {@link Optional}.
402      *
403      * @author Oliver Gierke
404      */

405     private static class NullableWrapperToGuavaOptionalConverter extends AbstractWrapperTypeConverter {
406
407         /**
408          * Creates a new {@link NullableWrapperToGuavaOptionalConverter} using the given {@link ConversionService}.
409          *
410          * @param conversionService must not be {@literal null}.
411          */

412         public NullableWrapperToGuavaOptionalConverter(ConversionService conversionService) {
413             super(conversionService, Optional.absent(), Collections.singleton(Optional.class));
414         }
415
416         /*
417          * (non-Javadoc)
418          * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
419          */

420         @Override
421         protected Object wrap(Object source) {
422             return Optional.of(source);
423         }
424
425         public static WrapperType getWrapperType() {
426             return WrapperType.singleValue(Optional.class);
427         }
428     }
429
430     /**
431      * A Spring {@link Converter} to support JDK 8's {@link java.util.Optional}.
432      *
433      * @author Oliver Gierke
434      */

435     private static class NullableWrapperToJdk8OptionalConverter extends AbstractWrapperTypeConverter {
436
437         /**
438          * Creates a new {@link NullableWrapperToJdk8OptionalConverter} using the given {@link ConversionService}.
439          *
440          * @param conversionService must not be {@literal null}.
441          */

442         public NullableWrapperToJdk8OptionalConverter(ConversionService conversionService) {
443             super(conversionService, java.util.Optional.empty());
444         }
445
446         /*
447          * (non-Javadoc)
448          * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
449          */

450         @Override
451         protected Object wrap(Object source) {
452             return java.util.Optional.of(source);
453         }
454
455         public static WrapperType getWrapperType() {
456             return WrapperType.singleValue(java.util.Optional.class);
457         }
458     }
459
460     /**
461      * A Spring {@link Converter} to support returning {@link Future} instances from repository methods.
462      *
463      * @author Oliver Gierke
464      */

465     private static class NullableWrapperToFutureConverter extends AbstractWrapperTypeConverter {
466
467         /**
468          * Creates a new {@link NullableWrapperToFutureConverter} using the given {@link ConversionService}.
469          *
470          * @param conversionService must not be {@literal null}.
471          */

472         public NullableWrapperToFutureConverter(ConversionService conversionService) {
473             super(conversionService, new AsyncResult<>(null), Arrays.asList(Future.class, ListenableFuture.class));
474         }
475
476         /*
477          * (non-Javadoc)
478          * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
479          */

480         @Override
481         protected Object wrap(Object source) {
482             return new AsyncResult<>(source);
483         }
484     }
485
486     /**
487      * A Spring {@link Converter} to support returning {@link CompletableFuture} instances from repository methods.
488      *
489      * @author Oliver Gierke
490      */

491     private static class NullableWrapperToCompletableFutureConverter extends AbstractWrapperTypeConverter {
492
493         /**
494          * Creates a new {@link NullableWrapperToCompletableFutureConverter} using the given {@link ConversionService}.
495          *
496          * @param conversionService must not be {@literal null}.
497          */

498         public NullableWrapperToCompletableFutureConverter(ConversionService conversionService) {
499             super(conversionService, CompletableFuture.completedFuture(null));
500         }
501
502         /*
503          * (non-Javadoc)
504          * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
505          */

506         @Override
507         protected Object wrap(Object source) {
508             return source instanceof CompletableFuture ? source : CompletableFuture.completedFuture(source);
509         }
510
511         public static WrapperType getWrapperType() {
512             return WrapperType.singleValue(CompletableFuture.class);
513         }
514     }
515
516     /**
517      * A Spring {@link Converter} to support Scala's {@link Option}.
518      *
519      * @author Oliver Gierke
520      * @since 1.13
521      */

522     private static class NullableWrapperToScalaOptionConverter extends AbstractWrapperTypeConverter {
523
524         public NullableWrapperToScalaOptionConverter(ConversionService conversionService) {
525             super(conversionService, Option.empty(), Collections.singleton(Option.class));
526         }
527
528         /*
529          * (non-Javadoc)
530          * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
531          */

532         @Override
533         protected Object wrap(Object source) {
534             return Option.apply(source);
535         }
536
537         public static WrapperType getWrapperType() {
538             return WrapperType.singleValue(Option.class);
539         }
540     }
541
542     /**
543      * Converter to convert from {@link NullableWrapper} into JavaSlang's {@link io.vavr.control.Option}.
544      *
545      * @author Oliver Gierke
546      * @since 2.0
547      */

548     private static class NullableWrapperToVavrOptionConverter extends AbstractWrapperTypeConverter {
549
550         /**
551          * Creates a new {@link NullableWrapperToJavaslangOptionConverter} using the given {@link ConversionService}.
552          *
553          * @param conversionService must not be {@literal null}.
554          */

555         public NullableWrapperToVavrOptionConverter(ConversionService conversionService) {
556             super(conversionService, io.vavr.control.Option.none(), Collections.singleton(io.vavr.control.Option.class));
557         }
558
559         public static WrapperType getWrapperType() {
560             return WrapperType.singleValue(io.vavr.control.Option.class);
561         }
562
563         /*
564          * (non-Javadoc)
565          * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
566          */

567         @Override
568         protected Object wrap(Object source) {
569             return io.vavr.control.Option.of(source);
570         }
571     }
572
573     /**
574      * A {@link Converter} to unwrap Guava {@link Optional} instances.
575      *
576      * @author Oliver Gierke
577      * @since 1.12
578      */

579     private enum GuavaOptionalUnwrapper implements Converter<Object, Object> {
580
581         INSTANCE;
582
583         /*
584          * (non-Javadoc)
585          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
586          */

587         @Nullable
588         @Override
589         public Object convert(Object source) {
590             return source instanceof Optional ? ((Optional<?>) source).orNull() : source;
591         }
592     }
593
594     /**
595      * A {@link Converter} to unwrap JDK 8 {@link java.util.Optional} instances.
596      *
597      * @author Oliver Gierke
598      * @since 1.12
599      */

600     private enum Jdk8OptionalUnwrapper implements Converter<Object, Object> {
601
602         INSTANCE;
603
604         /*
605          * (non-Javadoc)
606          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
607          */

608         @Nullable
609         @Override
610         public Object convert(Object source) {
611             return source instanceof java.util.Optional ? ((java.util.Optional<?>) source).orElse(null) : source;
612         }
613     }
614
615     /**
616      * A {@link Converter} to unwrap a Scala {@link Option} instance.
617      *
618      * @author Oliver Gierke
619      * @author Mark Paluch
620      * @since 1.12
621      */

622     private enum ScalOptionUnwrapper implements Converter<Object, Object> {
623
624         INSTANCE;
625
626         private final Function0<Object> alternative = new AbstractFunction0<Object>() {
627
628             /*
629              * (non-Javadoc)
630              * @see scala.Function0#apply()
631              */

632             @Nullable
633             @Override
634             public Option<Object> apply() {
635                 return null;
636             }
637         };
638
639         /*
640          * (non-Javadoc)
641          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
642          */

643         @Nullable
644         @Override
645         public Object convert(Object source) {
646             return source instanceof Option ? ((Option<?>) source).getOrElse(alternative) : source;
647         }
648     }
649
650     /**
651      * Converter to unwrap Vavr {@link io.vavr.control.Option} instances.
652      *
653      * @author Oliver Gierke
654      * @since 2.0
655      */

656     private enum VavrOptionUnwrapper implements Converter<Object, Object> {
657
658         INSTANCE;
659
660         /*
661          * (non-Javadoc)
662          * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
663          */

664         @Nullable
665         @Override
666         @SuppressWarnings("unchecked")
667         public Object convert(Object source) {
668
669             if (source instanceof io.vavr.control.Option) {
670                 return ((io.vavr.control.Option<Object>) source).getOrElse(() -> null);
671             }
672
673             if (source instanceof io.vavr.collection.Traversable) {
674                 return VavrCollections.ToJavaConverter.INSTANCE.convert(source);
675             }
676
677             return source;
678         }
679     }
680
681     @RequiredArgsConstructor
682     private static class IterableToStreamableConverter implements ConditionalGenericConverter {
683
684         private static final TypeDescriptor STREAMABLE = TypeDescriptor.valueOf(Streamable.class);
685
686         private final Map<TypeDescriptor, Boolean> TARGET_TYPE_CACHE = new ConcurrentHashMap<>();
687         private final ConversionService conversionService = DefaultConversionService.getSharedInstance();
688
689         /*
690          * (non-Javadoc)
691          * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
692          */

693         @org.springframework.lang.NonNull
694         @Override
695         public Set<ConvertiblePair> getConvertibleTypes() {
696             return Collections.singleton(new ConvertiblePair(Iterable.class, Object.class));
697         }
698
699         /*
700          * (non-Javadoc)
701          * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
702          */

703         @Override
704         public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
705
706             if (sourceType.isAssignableTo(targetType)) {
707                 return false;
708             }
709
710             if (!Iterable.class.isAssignableFrom(sourceType.getType())) {
711                 return false;
712             }
713
714             if (Streamable.class.equals(targetType.getType())) {
715                 return true;
716             }
717
718             return TARGET_TYPE_CACHE.computeIfAbsent(targetType, it -> {
719                 return conversionService.canConvert(STREAMABLE, targetType);
720             });
721         }
722
723         /*
724          * (non-Javadoc)
725          * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
726          */

727         @SuppressWarnings("unchecked")
728         @Nullable
729         @Override
730         public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
731
732             Streamable<Object> streamable = source == null //
733                     ? Streamable.empty() //
734                     : Streamable.of(Iterable.class.cast(source));
735
736             return Streamable.class.equals(targetType.getType()) //
737                     ? streamable //
738                     : conversionService.convert(streamable, STREAMABLE, targetType);
739         }
740     }
741
742     @Value
743     @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
744     public static class WrapperType {
745
746         enum Cardinality {
747             NONE, SINGLE, MULTI;
748         }
749
750         Class<?> type;
751         @Getter(AccessLevel.NONE) Cardinality cardinality;
752
753         public static WrapperType singleValue(Class<?> type) {
754             return new WrapperType(type, Cardinality.SINGLE);
755         }
756
757         public static WrapperType multiValue(Class<?> type) {
758             return new WrapperType(type, Cardinality.MULTI);
759         }
760
761         public static WrapperType noValue(Class<?> type) {
762             return new WrapperType(type, Cardinality.NONE);
763         }
764
765         boolean isSingleValue() {
766             return cardinality.equals(Cardinality.SINGLE);
767         }
768     }
769 }
770