From f933b4562efae355cecd09aa69eb10175fa0286a Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Sat, 11 Dec 2021 11:22:33 +0100 Subject: [PATCH] Complete Vavr collection detection on TypeInformation and CustomConversions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved Vavr collection converters into a type in the utility package. Register the converters via CustomConversions.registerConvertersIn(…) to make sure that all the Spring Data object mapping converters automatically benefit from a ConversionService that is capable of translating between Java-native collections and Vavr ones. Issue #2511. --- .../data/convert/CustomConversions.java | 16 ++--- .../util/QueryExecutionConverters.java | 19 ++++-- .../data/util/TypeDiscoverer.java | 48 +++++++------- .../VavrCollectionConverters.java} | 65 +++++++++++++++---- .../convert/CustomConversionsUnitTests.java | 17 ++++- 5 files changed, 106 insertions(+), 59 deletions(-) rename src/main/java/org/springframework/data/{repository/util/VavrCollections.java => util/VavrCollectionConverters.java} (68%) diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index e9fd8715d..6a97eb0cb 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; @@ -178,6 +169,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 cd7e74db4..00fbceec3 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()); @@ -408,23 +409,29 @@ public abstract class QueryExecutionConverters { INSTANCE; + private static final TypeDescriptor OBJECT_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); + /* * (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ @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 0803aadab..7198a11f2 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. @@ -25,16 +25,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; @@ -384,23 +375,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; - } - /* * (non-Javadoc) * @see org.springframework.data.util.TypeInformation#getComponentType() @@ -427,7 +405,7 @@ class TypeDiscoverer implements TypeInformation { return getTypeArgument(Iterable.class, 0); } - if(isNullableWrapper()) { + if (isNullableWrapper()) { return getTypeArgument(rawType, 0); } @@ -613,6 +591,24 @@ class TypeDiscoverer implements TypeInformation { return hashCode; } + /** + * Returns whether the current type is considered a collection. + * + * @return + */ + private boolean isCollection() { + + Class 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 68% rename from src/main/java/org/springframework/data/repository/util/VavrCollections.java rename to src/main/java/org/springframework/data/util/VavrCollectionConverters.java index e4e56d2d9..37156b9aa 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,50 +13,91 @@ * 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()); - INSTANCE; + public static Collection getConvertersToRegister() { - public WrapperType getWrapperType() { - return WrapperType.multiValue(io.vavr.collection.Traversable.class); + if (!VAVR_PRESENT) { + return Collections.emptyList(); } + return Arrays.asList(ToJavaConverter.INSTANCE, FromJavaConverter.INSTANCE); + } + + public enum ToJavaConverter implements ConditionalGenericConverter { + + INSTANCE; + + 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)); + /* * (non-Javadoc) - * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() */ @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()); + } + + /* + * (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) { + + return sourceType.isAssignableTo(TRAVERSAL_TYPE) + && COLLECTIONS_AND_MAP.contains(targetType.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) + */ + @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 c10a33935..c03409449 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; @@ -30,7 +31,6 @@ import org.jmolecules.ddd.types.Association; import org.jmolecules.ddd.types.Identifier; import org.joda.time.DateTime; 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; @@ -46,7 +46,6 @@ import org.springframework.data.convert.Jsr310Converters.LocalDateTimeToDateConv import org.springframework.data.convert.ThreeTenBackPortConverters.LocalDateTimeToJavaTimeInstantConverter; import org.springframework.data.geo.Point; import org.springframework.data.mapping.model.SimpleTypeHolder; - import org.threeten.bp.LocalDateTime; /** @@ -292,6 +291,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) { ProxyFactory factory = new ProxyFactory();