diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index 35daed439..e9a121063 100644 --- a/src/main/java/org/springframework/data/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2021 the original author or authors. + * Copyright 2011-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,7 @@ package org.springframework.data.convert; import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; @@ -33,7 +24,6 @@ import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.converter.Converter; @@ -46,6 +36,7 @@ import org.springframework.data.convert.ConverterBuilder.ConverterAware; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.Predicates; import org.springframework.data.util.Streamable; +import org.springframework.data.util.VavrCollectionConverters; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -176,6 +167,7 @@ public class CustomConversions { Assert.notNull(conversionService, "ConversionService must not be null!"); converters.forEach(it -> registerConverterIn(it, conversionService)); + VavrCollectionConverters.getConvertersToRegister().forEach(it -> registerConverterIn(it, conversionService)); } /** 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 506eb9a41..eb86e0c23 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ import org.springframework.data.util.NullableWrapperConverters; import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; +import org.springframework.data.util.VavrCollectionConverters; import org.springframework.lang.Nullable; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.util.Assert; @@ -99,7 +100,7 @@ public abstract class QueryExecutionConverters { if (VAVR_PRESENT) { - WRAPPER_TYPES.add(VavrCollections.ToJavaConverter.INSTANCE.getWrapperType()); + WRAPPER_TYPES.add(VavrTraversableUnwrapper.INSTANCE.getWrapperType()); UNWRAPPERS.add(VavrTraversableUnwrapper.INSTANCE); // Try support @@ -196,7 +197,7 @@ public abstract class QueryExecutionConverters { NullableWrapperConverters.registerConvertersIn(conversionService); if (VAVR_PRESENT) { - conversionService.addConverter(VavrCollections.FromJavaConverter.INSTANCE); + conversionService.addConverter(VavrCollectionConverters.FromJavaConverter.INSTANCE); } conversionService.addConverter(new NullableWrapperToCompletableFutureConverter()); @@ -391,19 +392,25 @@ public abstract class QueryExecutionConverters { INSTANCE; + private static final TypeDescriptor OBJECT_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); + @Nullable @Override - @SuppressWarnings("unchecked") + @SuppressWarnings("null") public Object convert(Object source) { if (source instanceof io.vavr.collection.Traversable) { - return VavrCollections.ToJavaConverter.INSTANCE.convert(source); + return VavrCollectionConverters.ToJavaConverter.INSTANCE // + .convert(source, TypeDescriptor.forObject(source), OBJECT_DESCRIPTOR); } return source; } - } + public WrapperType getWrapperType() { + return WrapperType.multiValue(io.vavr.collection.Traversable.class); + } + } private static class IterableToStreamableConverter implements ConditionalGenericConverter { diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java index 065539c19..606423386 100644 --- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2021 the original author or authors. + * Copyright 2011-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,16 +23,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import java.util.ArrayList; -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.Optional; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -347,23 +338,10 @@ class TypeDiscoverer implements TypeInformation { return rawType.isArray() // || Iterable.class.equals(rawType) // - || Streamable.class.isAssignableFrom(rawType) + || Streamable.class.isAssignableFrom(rawType) // || isCollection(); } - private boolean isCollection() { - - Class type = getType(); - - for (Class collectionType : COLLECTION_TYPES) { - if (collectionType.isAssignableFrom(type)) { - return true; - } - } - - return false; - } - @Nullable public final TypeInformation getComponentType() { return componentType.orElse(null); @@ -386,7 +364,7 @@ class TypeDiscoverer implements TypeInformation { return getTypeArgument(Iterable.class, 0); } - if(isNullableWrapper()) { + if (isNullableWrapper()) { return getTypeArgument(rawType, 0); } @@ -541,6 +519,24 @@ class TypeDiscoverer implements TypeInformation { return hashCode; } + /** + * Returns whether the current type is considered a collection. + * + * @return + */ + private boolean isCollection() { + + var type = getType(); + + for (Class collectionType : COLLECTION_TYPES) { + if (collectionType.isAssignableFrom(type)) { + return true; + } + } + + return false; + } + /** * A synthetic {@link ParameterizedType}. * diff --git a/src/main/java/org/springframework/data/repository/util/VavrCollections.java b/src/main/java/org/springframework/data/util/VavrCollectionConverters.java similarity index 71% rename from src/main/java/org/springframework/data/repository/util/VavrCollections.java rename to src/main/java/org/springframework/data/util/VavrCollectionConverters.java index 5deb0a184..523991f56 100644 --- a/src/main/java/org/springframework/data/repository/util/VavrCollections.java +++ b/src/main/java/org/springframework/data/util/VavrCollectionConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,46 +13,79 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.util; +package org.springframework.data.util; import io.vavr.collection.LinkedHashMap; import io.vavr.collection.LinkedHashSet; import io.vavr.collection.Traversable; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.repository.util.QueryExecutionConverters.WrapperType; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Converter implementations to map from and to Vavr collections. * * @author Oliver Gierke * @author Christoph Strobl - * @since 2.0 + * @since 2.7 */ -class VavrCollections { +public class VavrCollectionConverters { - public enum ToJavaConverter implements Converter { + private static final boolean VAVR_PRESENT = ClassUtils.isPresent("io.vavr.control.Option", + NullableWrapperConverters.class.getClassLoader()); + + public static Collection getConvertersToRegister() { + + if (!VAVR_PRESENT) { + return Collections.emptyList(); + } + + return Arrays.asList(ToJavaConverter.INSTANCE, FromJavaConverter.INSTANCE); + } + + public enum ToJavaConverter implements ConditionalGenericConverter { INSTANCE; - public WrapperType getWrapperType() { - return WrapperType.multiValue(io.vavr.collection.Traversable.class); - } + private static final TypeDescriptor TRAVERSAL_TYPE = TypeDescriptor.valueOf(Traversable.class); + private static final Set> COLLECTIONS_AND_MAP = new HashSet<>( + Arrays.asList(Collection.class, List.class, Set.class, Map.class)); @NonNull @Override - public Object convert(Object source) { + public Set getConvertibleTypes() { + + return COLLECTIONS_AND_MAP.stream() + .map(it -> new ConvertiblePair(Traversable.class, it)) + .collect(Collectors.toSet()); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + + return sourceType.isAssignableTo(TRAVERSAL_TYPE) + && COLLECTIONS_AND_MAP.contains(targetType.getType()); + } + + @Nullable + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (source == null) { + return null; + } if (source instanceof io.vavr.collection.Seq) { return ((io.vavr.collection.Seq) source).asJava(); diff --git a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java index bf05b0704..ab4a34cf8 100644 --- a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java +++ b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2021 the original author or authors. + * Copyright 2011-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Predicate; @@ -29,7 +30,6 @@ import java.util.function.Predicate; import org.jmolecules.ddd.types.Association; import org.jmolecules.ddd.types.Identifier; import org.junit.jupiter.api.Test; - import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @@ -261,6 +261,18 @@ class CustomConversionsUnitTests { assertThat(conversions.hasCustomReadTarget(String.class, Identifier.class)).isTrue(); } + @Test // GH-2511 + void registersVavrConverters() { + + ConfigurableConversionService conversionService = new DefaultConversionService(); + + new CustomConversions(StoreConversions.NONE, Collections.emptyList()) + .registerConvertersIn(conversionService); + + assertThat(conversionService.canConvert(io.vavr.collection.List.class, List.class)).isTrue(); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class)).isTrue(); + } + private static Class createProxyTypeFor(Class type) { var factory = new ProxyFactory(); diff --git a/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java b/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java index 502cbff07..2349e6dcf 100755 --- a/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java +++ b/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java @@ -179,34 +179,34 @@ public class TypeDiscovererUnitTests { assertThat(type.isSubTypeOf(String.class)).isFalse(); } - @Test + @Test // #2511 void considerVavrMapToBeAMap() { - TypeInformation type = from(io.vavr.collection.Map.class); + var type = from(io.vavr.collection.Map.class); assertThat(type.isMap()).isTrue(); } - @Test + @Test // #2511 void considerVavrSetToBeCollectionLike() { - TypeInformation type = from(io.vavr.collection.Set.class); + var type = from(io.vavr.collection.Set.class); assertThat(type.isCollectionLike()).isTrue(); } - @Test + @Test // #2511 void considerVavrSeqToBeCollectionLike() { - TypeInformation type = from(io.vavr.collection.Seq.class); + var type = from(io.vavr.collection.Seq.class); assertThat(type.isCollectionLike()).isTrue(); } - @Test + @Test // #2511 void considerVavrListToBeCollectionLike() { - TypeInformation type = from(io.vavr.collection.List.class); + var type = from(io.vavr.collection.List.class); assertThat(type.isCollectionLike()).isTrue(); }