From 6ebe288f83dcbcdfa642b7ad2068763eee9a48ef Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 2 May 2022 23:23:52 +0200 Subject: [PATCH] Improve custom collection support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom collection support is now centralized in ….util.CustomCollections. It exposes API to detect whether a type is a map or collection, identifies the map base type etc. Support for different collection implementations is externalized via the CustomCollectionRegistrar SPI that allows to define implementations via spring.factories. The current support for Vavr collections has been moved into an implementation of that, VavrCollections. Unit tests for custom collection handling and conversion previously living in QueryExecutionConverterUnitTests have been moved into CustomCollectionsUnitTests. Fixes #2619. --- .../data/convert/CustomConversions.java | 18 +- .../util/QueryExecutionConverters.java | 67 +-- .../data/util/CustomCollectionRegistrar.java | 90 ++++ .../data/util/CustomCollections.java | 491 ++++++++++++++++++ .../util/ParameterizedTypeInformation.java | 10 +- .../data/util/TypeDiscoverer.java | 127 +---- .../data/util/VavrCollectionConverters.java | 205 -------- src/main/resources/META-INF/spring.factories | 1 + .../QueryExecutionConvertersUnitTests.java | 73 --- .../data/util/CustomCollectionsUnitTests.java | 238 +++++++++ 10 files changed, 861 insertions(+), 459 deletions(-) create mode 100644 src/main/java/org/springframework/data/util/CustomCollectionRegistrar.java create mode 100644 src/main/java/org/springframework/data/util/CustomCollections.java delete mode 100644 src/main/java/org/springframework/data/util/VavrCollectionConverters.java create mode 100644 src/test/java/org/springframework/data/util/CustomCollectionsUnitTests.java diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index cacd9c0cc..b251fcc04 100644 --- a/src/main/java/org/springframework/data/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -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; @@ -44,9 +34,9 @@ import org.springframework.core.convert.converter.GenericConverter.ConvertiblePa import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.convert.ConverterBuilder.ConverterAware; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.CustomCollections; 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; @@ -182,7 +172,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)); + CustomCollections.registerConvertersIn(conversionService); } @Nullable @@ -844,7 +834,7 @@ public class CustomConversions { * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; 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 00fbceec3..b8e4c8b8c 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -26,6 +26,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; +import java.util.function.Function; import java.util.stream.Stream; import org.springframework.core.convert.ConversionService; @@ -38,12 +39,13 @@ 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; +import org.springframework.data.util.CustomCollections; import org.springframework.data.util.NullableWrapper; 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.NonNull; import org.springframework.lang.Nullable; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.util.Assert; @@ -80,7 +82,7 @@ public abstract class QueryExecutionConverters { private static final Set WRAPPER_TYPES = new HashSet<>(); private static final Set UNWRAPPER_TYPES = new HashSet(); - private static final Set> UNWRAPPERS = new HashSet<>(); + private static final Set> UNWRAPPERS = new HashSet<>(); private static final Set> ALLOWED_PAGEABLE_TYPES = new HashSet<>(); private static final Map, ExecutionAdapter> EXECUTION_ADAPTER = new HashMap<>(); private static final Map, Boolean> supportsCache = new ConcurrentReferenceHashMap<>(); @@ -98,16 +100,19 @@ public abstract class QueryExecutionConverters { WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); - if (VAVR_PRESENT) { + UNWRAPPERS.addAll(CustomCollections.getUnwrappers()); + + CustomCollections.getCustomTypes().stream() + .map(WrapperType::multiValue) + .forEach(WRAPPER_TYPES::add); - WRAPPER_TYPES.add(VavrTraversableUnwrapper.INSTANCE.getWrapperType()); - UNWRAPPERS.add(VavrTraversableUnwrapper.INSTANCE); + CustomCollections.getPaginationReturnTypes().forEach(ALLOWED_PAGEABLE_TYPES::add); + + if (VAVR_PRESENT) { // Try support WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class)); EXECUTION_ADAPTER.put(io.vavr.control.Try.class, it -> io.vavr.control.Try.of(it::get)); - - ALLOWED_PAGEABLE_TYPES.add(io.vavr.collection.Seq.class); } } @@ -195,10 +200,7 @@ public abstract class QueryExecutionConverters { conversionService.removeConvertible(Collection.class, Object.class); NullableWrapperConverters.registerConvertersIn(conversionService); - - if (VAVR_PRESENT) { - conversionService.addConverter(VavrCollectionConverters.FromJavaConverter.INSTANCE); - } + CustomCollections.registerConvertersIn(conversionService); conversionService.addConverter(new NullableWrapperToCompletableFutureConverter()); conversionService.addConverter(new NullableWrapperToFutureConverter()); @@ -220,9 +222,9 @@ public abstract class QueryExecutionConverters { return source; } - for (Converter converter : UNWRAPPERS) { + for (Function converter : UNWRAPPERS) { - Object result = converter.convert(source); + Object result = converter.apply(source); if (result != source) { return result; @@ -310,7 +312,7 @@ public abstract class QueryExecutionConverters { * (non-Javadoc) * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() */ - + @NonNull @Override public Set getConvertibleTypes() { @@ -399,40 +401,6 @@ public abstract class QueryExecutionConverters { } } - /** - * Converter to unwrap Vavr {@link io.vavr.collection.Traversable} instances. - * - * @author Oliver Gierke - * @since 2.0 - */ - private enum VavrTraversableUnwrapper implements Converter { - - 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("null") - public Object convert(Object source) { - - if (source instanceof io.vavr.collection.Traversable) { - 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 { private static final TypeDescriptor STREAMABLE = TypeDescriptor.valueOf(Streamable.class); @@ -446,6 +414,7 @@ public abstract class QueryExecutionConverters { * (non-Javadoc) * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() */ + @NonNull @Override public Set getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Iterable.class, Object.class)); @@ -514,7 +483,7 @@ public abstract class QueryExecutionConverters { * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; diff --git a/src/main/java/org/springframework/data/util/CustomCollectionRegistrar.java b/src/main/java/org/springframework/data/util/CustomCollectionRegistrar.java new file mode 100644 index 000000000..43bbb161a --- /dev/null +++ b/src/main/java/org/springframework/data/util/CustomCollectionRegistrar.java @@ -0,0 +1,90 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.core.convert.converter.ConverterRegistry; + +/** + * An SPI to register custom collection types. Implementations need to be registered via + * {@code META-INF/spring.factories}. + * + * @author Oliver Drotbohm + * @since 2.7 + */ +public interface CustomCollectionRegistrar { + + /** + * Returns whether the registrar is available, meaning whether it can be used at runtime. Primary use is for + * implementations that need to perform a classpath check to prevent the actual methods loading classes that might not + * be available. + * + * @return whether the registrar is available + */ + default boolean isAvailable() { + return true; + } + + /** + * Returns all types that are supposed to be considered maps. Primary requirement is key and value generics expressed + * in the first and second generics parameter of the type. Also, the types should be transformable into their + * Java-native equivalent using {@link #toJavaNativeCollection()}. + * + * @return will never be {@literal null}. + * @see #toJavaNativeCollection() + */ + Collection> getMapTypes(); + + /** + * Returns all types that are supposed to be considered collections. Primary requirement is that their component types + * are expressed as first generics parameter. Also, the types should be transformable into their Java-native + * equivalent using {@link #toJavaNativeCollection()}. + * + * @return will never be {@literal null}. + * @see #toJavaNativeCollection() + */ + Collection> getCollectionTypes(); + + /** + * Return all types that are considered valid return types for methods using pagination. These are usually collections + * with a stable order, like {@link List} but no {@link Set}s, as pagination usually involves sorting. + * + * @return will never be {@literal null}. + */ + default Collection> getAllowedPaginationReturnTypes() { + return Collections.emptyList(); + } + + /** + * Register all converters to convert instances of the types returned by {@link #getCollectionTypes()} and + * {@link #getMapTypes()} from an to their Java-native counterparts. + * + * @param registry will never be {@literal null}. + */ + void registerConvertersIn(ConverterRegistry registry); + + /** + * Returns a {@link Function} to convert instances of their Java-native counterpart. + * + * @return must not be {@literal null}. + */ + Function toJavaNativeCollection(); +} diff --git a/src/main/java/org/springframework/data/util/CustomCollections.java b/src/main/java/org/springframework/data/util/CustomCollections.java new file mode 100644 index 000000000..5e8c6be7c --- /dev/null +++ b/src/main/java/org/springframework/data/util/CustomCollections.java @@ -0,0 +1,491 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util; + +import io.vavr.collection.HashMap; +import io.vavr.collection.LinkedHashMap; +import io.vavr.collection.LinkedHashSet; +import io.vavr.collection.Seq; +import io.vavr.collection.Traversable; + +import java.util.ArrayList; +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.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Central API to expose information about custom collections present for Spring Data. Exposes custom collection and map + * types and registers converters to convert them from and to Java-native collections. + * + * @author Oliver Drotbohm + * @since 2.7 + * @soundtrack Black Sea Dahu - White Creatures (White Creatures) + */ +public class CustomCollections { + + private static final Set> CUSTOM_TYPES, CUSTOM_MAP_TYPES, CUSTOM_COLLECTION_TYPES, PAGINATION_RETURN_TYPES; + private static final Set> COLLECTIONS_AND_MAP = new HashSet<>( + Arrays.asList(Collection.class, List.class, Set.class, Map.class)); + private static final SearchableTypes MAP_TYPES, COLLECTION_TYPES; + private static final Collection REGISTRARS; + + static { + + CUSTOM_TYPES = new HashSet<>(); + PAGINATION_RETURN_TYPES = new HashSet<>(); + CUSTOM_MAP_TYPES = new HashSet<>(); + CUSTOM_COLLECTION_TYPES = new HashSet<>(); + + REGISTRARS = SpringFactoriesLoader + .loadFactories(CustomCollectionRegistrar.class, CustomCollections.class.getClassLoader()) + .stream() + .filter(CustomCollectionRegistrar::isAvailable) + .collect(Collectors.toList()); + + REGISTRARS.forEach(it -> { + + it.getCollectionTypes().forEach(CustomCollections::registerCollectionType); + it.getMapTypes().forEach(CustomCollections::registerMapType); + it.getAllowedPaginationReturnTypes().forEach(PAGINATION_RETURN_TYPES::add); + }); + + MAP_TYPES = new SearchableTypes(CUSTOM_MAP_TYPES, Map.class); + COLLECTION_TYPES = new SearchableTypes(CUSTOM_COLLECTION_TYPES, Collection.class); + } + + /** + * Returns all custom collection and map types. + * + * @return will never be {@literal null}. + */ + public static Set> getCustomTypes() { + return CUSTOM_TYPES; + } + + /** + * Returns all types that are allowed pagination return types. + * + * @return will never be {@literal null}. + */ + public static Set> getPaginationReturnTypes() { + return PAGINATION_RETURN_TYPES; + } + + /** + * Returns whether the given type is a map base type. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static boolean isMapBaseType(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return MAP_TYPES.has(type); + } + + /** + * Returns the map base type for the given type, i.e. the one that's considered the logical map interface ({@link Map} + * for {@link HashMap} etc.). + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + * @throws IllegalArgumentException in case we do not find a map base type for the given one. + */ + public static Class getMapBaseType(Class type) throws IllegalArgumentException { + return MAP_TYPES.getSuperType(type); + } + + /** + * Returns whether the given type is considered a {@link Map} type. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static boolean isMap(Class type) { + return MAP_TYPES.hasSuperTypeFor(type); + } + + /** + * Returns whether the given type is considered a {@link Collection} type. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static boolean isCollection(Class type) { + return COLLECTION_TYPES.hasSuperTypeFor(type); + } + + /** + * Returns all unwrapper functions that transform the custom collections into Java-native ones. + * + * @return will never be {@literal null}. + */ + public static Set> getUnwrappers() { + + return REGISTRARS.stream() + .map(CustomCollectionRegistrar::toJavaNativeCollection) + .collect(Collectors.toSet()); + } + + /** + * Registers all converters to transform Java-native collections into custom ones and back in the given + * {@link ConverterRegistry}. + * + * @param registry must not be {@literal null}. + */ + public static void registerConvertersIn(ConverterRegistry registry) { + + Assert.notNull(registry, "ConverterRegistry must not be null!"); + + // Remove general collection to anything conversion as that would also convert collections to maps + registry.removeConvertible(Collection.class, Object.class); + + REGISTRARS.forEach(it -> it.registerConvertersIn(registry)); + } + + private static void registerCollectionType(Class type) { + + CUSTOM_TYPES.add(type); + CUSTOM_COLLECTION_TYPES.add(type); + } + + private static void registerMapType(Class type) { + + CUSTOM_TYPES.add(type); + CUSTOM_MAP_TYPES.add(type); + } + + private static class SearchableTypes { + + private static final BiPredicate, Class> EQUALS = (left, right) -> left.equals(right); + private static final BiPredicate, Class> IS_ASSIGNABLE = (left, right) -> left.isAssignableFrom(right); + private static final Function, Boolean> IS_NOT_NULL = it -> it != null; + + private final Collection> types; + + public SearchableTypes(Set> types, Class... additional) { + + List> all = new ArrayList<>(Arrays.asList(additional)); + all.addAll(types); + + this.types = all; + } + + public boolean hasSuperTypeFor(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return isOneOf(type, IS_ASSIGNABLE, IS_NOT_NULL); + } + + /** + * Returns whether the current's raw type is one of the given ones. + * + * @param candidates must not be {@literal null}. + * @return + */ + public boolean has(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return isOneOf(type, EQUALS, IS_NOT_NULL); + } + + /** + * Returns the super type of the given one from the set of types. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + * @throws IllegalArgumentException in case no base type of the given one can be found. + */ + public Class getSuperType(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + Supplier message = () -> String.format("Type %s not contained in candidates %s!", type, types); + + return isOneOf(type, (l, r) -> l.isAssignableFrom(r), rejectNull(message)); + } + + /** + * Returns whether the given type matches one of the given candidates given the matcher with the + * + * @param + * @param type the type to match against the current candidates. + * @param matcher how to match the candidates against the given type. + * @param resultMapper a {@link Function} to map the potentially given type to the actual result. + * @return will never be {@literal null}. + */ + private T isOneOf(Class type, BiPredicate, Class> matcher, Function, T> resultMapper) { + + for (Class candidate : types) { + if (matcher.test(candidate, type)) { + return resultMapper.apply(candidate); + } + } + + return resultMapper.apply(null); + } + + /** + * Returns a function that rejects the source {@link Class} resolving the given message if the former is + * {@literal null}. + * + * @param message must not be {@literal null}. + * @return will never be {@literal null}. + */ + private static Function, Class> rejectNull(Supplier message) { + + Assert.notNull(message, "Message must not be null!"); + + return candidate -> { + + if (candidate == null) { + throw new IllegalArgumentException(message.get()); + } + + return candidate; + }; + } + } + + static class VavrCollections implements CustomCollectionRegistrar { + + private static final TypeDescriptor OBJECT_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); + + /* + * (non-Javadoc) + * @see org.springframework.data.util.CustomCollectionRegistrar#isAvailable() + */ + @Override + public boolean isAvailable() { + return ClassUtils.isPresent("io.vavr.control.Option", VavrCollections.class.getClassLoader()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.CustomCollectionRegistrar#getMapTypes() + */ + @Override + public Collection> getMapTypes() { + return Collections.singleton(io.vavr.collection.Map.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.CustomCollectionRegistrar#getCollectionTypes() + */ + @Override + public Collection> getCollectionTypes() { + return Arrays.asList(Seq.class, io.vavr.collection.Set.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.CustomCollectionRegistrar#getAllowedPaginationReturnTypes() + */ + @Override + public Collection> getAllowedPaginationReturnTypes() { + return Collections.singleton(Seq.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.CustomCollectionRegistrar#registerConvertersIn(org.springframework.core.convert.converter.ConverterRegistry) + */ + @Override + public void registerConvertersIn(ConverterRegistry registry) { + + registry.addConverter(JavaToVavrCollectionConverter.INSTANCE); + registry.addConverter(VavrToJavaCollectionConverter.INSTANCE); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.CustomCollectionRegistrar#toJavaNativeCollection() + */ + @Override + public Function toJavaNativeCollection() { + + return source -> source instanceof io.vavr.collection.Traversable + ? VavrToJavaCollectionConverter.INSTANCE.convert(source, TypeDescriptor.forObject(source), OBJECT_DESCRIPTOR) + : source; + } + + private enum VavrToJavaCollectionConverter implements ConditionalGenericConverter { + + INSTANCE; + + private static final TypeDescriptor TRAVERSAL_TYPE = TypeDescriptor.valueOf(Traversable.class); + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() + */ + @NonNull + @Override + 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(); + } + + if (source instanceof io.vavr.collection.Map) { + return ((io.vavr.collection.Map) source).toJavaMap(); + } + + if (source instanceof io.vavr.collection.Set) { + return ((io.vavr.collection.Set) source).toJavaSet(); + } + + throw new IllegalArgumentException("Unsupported Vavr collection " + source.getClass()); + } + } + + private enum JavaToVavrCollectionConverter implements ConditionalGenericConverter { + + INSTANCE; + + private static final Set CONVERTIBLE_PAIRS; + + static { + + Set pairs = new HashSet<>(); + pairs.add(new ConvertiblePair(Collection.class, io.vavr.collection.Traversable.class)); + pairs.add(new ConvertiblePair(Map.class, io.vavr.collection.Traversable.class)); + + CONVERTIBLE_PAIRS = Collections.unmodifiableSet(pairs); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() + */ + @NonNull + @Override + public java.util.Set getConvertibleTypes() { + return CONVERTIBLE_PAIRS; + } + + /* + * (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) { + + // Prevent collections to be mapped to maps + if (sourceType.isCollection() && io.vavr.collection.Map.class.isAssignableFrom(targetType.getType())) { + return false; + } + + // Prevent maps to be mapped to collections + if (sourceType.isMap() && !(io.vavr.collection.Map.class.isAssignableFrom(targetType.getType()) + || targetType.getType().equals(Traversable.class))) { + return false; + } + + return true; + } + + /* + * (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 sourceDescriptor, TypeDescriptor targetDescriptor) { + + Class targetType = targetDescriptor.getType(); + + if (io.vavr.collection.Seq.class.isAssignableFrom(targetType)) { + return io.vavr.collection.List.ofAll((Iterable) source); + } + + if (io.vavr.collection.Set.class.isAssignableFrom(targetType)) { + return LinkedHashSet.ofAll((Iterable) source); + } + + if (io.vavr.collection.Map.class.isAssignableFrom(targetType)) { + return LinkedHashMap.ofAll((Map) source); + } + + // No dedicated type asked for, probably Traversable. + // Try to stay as close to the source value. + + if (source instanceof List) { + return io.vavr.collection.List.ofAll((Iterable) source); + } + + if (source instanceof Set) { + return LinkedHashSet.ofAll((Iterable) source); + } + + if (source instanceof Map) { + return LinkedHashMap.ofAll((Map) source); + } + + return source; + } + } + } +} diff --git a/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java b/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java index 3ae37f6d8..44b37a9d9 100644 --- a/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java +++ b/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java @@ -20,7 +20,6 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -134,7 +133,8 @@ class ParameterizedTypeInformation extends ParentTypeAwareTypeInformation : target.getSuperTypeInformation(rawType); List> myParameters = getTypeArguments(); - List> typeParameters = otherTypeInformation == null ? Collections.emptyList() + List> typeParameters = otherTypeInformation == null // + ? java.util.Collections.emptyList() // : otherTypeInformation.getTypeArguments(); if (myParameters.size() != typeParameters.size()) { @@ -158,8 +158,10 @@ class ParameterizedTypeInformation extends ParentTypeAwareTypeInformation @Nullable protected TypeInformation doGetComponentType() { - return isMap() && !isMapBaseType() - ? getRequiredSuperTypeInformation(getMapBaseType()).getComponentType() + Class type = getType(); + + return isMap() && !CustomCollections.isMapBaseType(type) + ? getRequiredSuperTypeInformation(CustomCollections.getMapBaseType(type)).getComponentType() : createInfo(this.type.getActualTypeArguments()[0]); } diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java index 6a24e8a94..8569643f6 100644 --- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -25,7 +25,12 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -35,7 +40,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -50,36 +54,6 @@ import org.springframework.util.ReflectionUtils; */ class TypeDiscoverer implements TypeInformation { - protected static final Class[] MAP_TYPES; - private static final Class[] COLLECTION_TYPES; - - static { - - ClassLoader classLoader = TypeDiscoverer.class.getClassLoader(); - - Set> mapTypes = new HashSet<>(); - mapTypes.add(Map.class); - - try { - mapTypes.add(ClassUtils.forName("io.vavr.collection.Map", classLoader)); - } catch (ClassNotFoundException o_O) {} - - MAP_TYPES = mapTypes.toArray(new Class[0]); - - Set> collectionTypes = new HashSet<>(); - collectionTypes.add(Collection.class); - - try { - collectionTypes.add(ClassUtils.forName("io.vavr.collection.Seq", classLoader)); - } catch (ClassNotFoundException o_O) {} - - try { - collectionTypes.add(ClassUtils.forName("io.vavr.collection.Set", classLoader)); - } catch (ClassNotFoundException o_O) {} - - COLLECTION_TYPES = collectionTypes.toArray(new Class[0]); - } - private final Type type; private final Map, Type> typeVariableMap; private final Map>> fieldTypes = new ConcurrentHashMap<>(); @@ -343,16 +317,7 @@ class TypeDiscoverer implements TypeInformation { * @see org.springframework.data.util.TypeInformation#isMap() */ public boolean isMap() { - - Class type = getType(); - - for (Class mapType : MAP_TYPES) { - if (mapType.isAssignableFrom(type)) { - return true; - } - } - - return false; + return CustomCollections.isMap(getType()); } /* @@ -366,7 +331,9 @@ class TypeDiscoverer implements TypeInformation { @Nullable protected TypeInformation doGetMapValueType() { - return isMap() ? getTypeArgument(getMapBaseType(), 1) + + return isMap() // + ? getTypeArgument(CustomCollections.getMapBaseType(getType()), 1) : getTypeArguments().stream().skip(1).findFirst().orElse(null); } @@ -381,7 +348,7 @@ class TypeDiscoverer implements TypeInformation { return rawType.isArray() // || Iterable.class.equals(rawType) // || Streamable.class.isAssignableFrom(rawType) // - || isCollection(); + || CustomCollections.isCollection(rawType); } /* @@ -403,7 +370,7 @@ class TypeDiscoverer implements TypeInformation { } if (isMap()) { - return getTypeArgument(getMapBaseType(), 0); + return getTypeArgument(CustomCollections.getMapBaseType(rawType), 0); } if (Iterable.class.isAssignableFrom(rawType)) { @@ -492,7 +459,7 @@ class TypeDiscoverer implements TypeInformation { * @see org.springframework.data.util.TypeInformation#getTypeParameters() */ public List> getTypeArguments() { - return Collections.emptyList(); + return java.util.Collections.emptyList(); } /* (non-Javadoc) @@ -538,14 +505,6 @@ class TypeDiscoverer implements TypeInformation { : null; } - protected boolean isMapBaseType() { - return isOneOf(MAP_TYPES); - } - - protected Class getMapBaseType() { - return getSuperTypeWithin(MAP_TYPES); - } - protected ResolvableType toResolvableType() { return ResolvableType.forType(type); } @@ -587,66 +546,6 @@ 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; - } - - /** - * Returns whether the current's raw type is one of the given ones. - * - * @param candidates must not be {@literal null}. - * @return - */ - private boolean isOneOf(Class[] candidates) { - - Assert.notNull(candidates, "Candidates must not be null!"); - - Class type = getType(); - - for (Class candidate : candidates) { - if (candidate.equals(type)) { - return true; - } - } - - return false; - } - - /** - * Returns the super type of the current raw type from the given candidates. - * - * @param candidates must not be {@literal null}. - * @return - */ - private Class getSuperTypeWithin(Class[] candidates) { - - Assert.notNull(candidates, "Candidates must not be null!"); - - Class type = getType(); - - for (Class candidate : candidates) { - if (candidate.isAssignableFrom(type)) { - return candidate; - } - } - - throw new IllegalArgumentException(String.format("Type %s not contained in candidates %s!", type, candidates)); - } - private boolean isNullableWrapper() { return NullableWrapperConverters.supports(getType()); } diff --git a/src/main/java/org/springframework/data/util/VavrCollectionConverters.java b/src/main/java/org/springframework/data/util/VavrCollectionConverters.java deleted file mode 100644 index 64add305c..000000000 --- a/src/main/java/org/springframework/data/util/VavrCollectionConverters.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2022-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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -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.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.7 - */ -public class VavrCollectionConverters { - - 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; - - 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.GenericConverter#getConvertibleTypes() - */ - @NonNull - @Override - 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(); - } - - if (source instanceof io.vavr.collection.Map) { - return ((io.vavr.collection.Map) source).toJavaMap(); - } - - if (source instanceof io.vavr.collection.Set) { - return ((io.vavr.collection.Set) source).toJavaSet(); - } - - throw new IllegalArgumentException("Unsupported Vavr collection " + source.getClass()); - } - } - - public enum FromJavaConverter implements ConditionalGenericConverter { - - INSTANCE { - - /* - * (non-Javadoc) - * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() - */ - @NonNull - @Override - public java.util.Set getConvertibleTypes() { - return CONVERTIBLE_PAIRS; - } - - /* - * (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) { - - // Prevent collections to be mapped to maps - if (sourceType.isCollection() && io.vavr.collection.Map.class.isAssignableFrom(targetType.getType())) { - return false; - } - - // Prevent maps to be mapped to collections - if (sourceType.isMap() && !(io.vavr.collection.Map.class.isAssignableFrom(targetType.getType()) - || targetType.getType().equals(Traversable.class))) { - return false; - } - - return true; - } - - /* - * (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 sourceDescriptor, TypeDescriptor targetDescriptor) { - - Class targetType = targetDescriptor.getType(); - - if (io.vavr.collection.Seq.class.isAssignableFrom(targetType)) { - return io.vavr.collection.List.ofAll((Iterable) source); - } - - if (io.vavr.collection.Set.class.isAssignableFrom(targetType)) { - return LinkedHashSet.ofAll((Iterable) source); - } - - if (io.vavr.collection.Map.class.isAssignableFrom(targetType)) { - return LinkedHashMap.ofAll((Map) source); - } - - // No dedicated type asked for, probably Traversable. - // Try to stay as close to the source value. - - if (source instanceof List) { - return io.vavr.collection.List.ofAll((Iterable) source); - } - - if (source instanceof Set) { - return LinkedHashSet.ofAll((Iterable) source); - } - - if (source instanceof Map) { - return LinkedHashMap.ofAll((Map) source); - } - - return source; - } - }; - - private static final Set CONVERTIBLE_PAIRS; - - static { - - Set pairs = new HashSet<>(); - pairs.add(new ConvertiblePair(Collection.class, io.vavr.collection.Traversable.class)); - pairs.add(new ConvertiblePair(Map.class, io.vavr.collection.Traversable.class)); - - CONVERTIBLE_PAIRS = Collections.unmodifiableSet(pairs); - } - } -} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index 5b6d502f7..45535f3c8 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -1 +1,2 @@ org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration +org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections 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 8807b2830..e576a208c 100755 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -32,10 +32,8 @@ import scala.Option; 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; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -187,77 +185,6 @@ class QueryExecutionConvertersUnitTests { assertThat(QueryExecutionConverters.unwrap(io.vavr.control.Option.of("string"))).isEqualTo("string"); } - @Test // DATACMNS-1065 - void conversListToVavr() { - - assertThat(conversionService.canConvert(List.class, io.vavr.collection.Traversable.class)).isTrue(); - assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class)).isTrue(); - assertThat(conversionService.canConvert(List.class, io.vavr.collection.Set.class)).isTrue(); - assertThat(conversionService.canConvert(List.class, io.vavr.collection.Map.class)).isFalse(); - - List integers = Arrays.asList(1, 2, 3); - - io.vavr.collection.Traversable result = conversionService.convert(integers, - io.vavr.collection.Traversable.class); - - assertThat(result).isInstanceOf(io.vavr.collection.List.class); - } - - @Test // DATACMNS-1065 - void convertsSetToVavr() { - - assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Traversable.class)).isTrue(); - assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Set.class)).isTrue(); - assertThat(conversionService.canConvert(Set.class, io.vavr.collection.List.class)).isTrue(); - assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Map.class)).isFalse(); - - Set integers = Collections.singleton(1); - - io.vavr.collection.Traversable result = conversionService.convert(integers, - io.vavr.collection.Traversable.class); - - assertThat(result).isInstanceOf(io.vavr.collection.Set.class); - } - - @Test // DATACMNS-1065 - void convertsMapToVavr() { - - assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Traversable.class)).isTrue(); - assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Map.class)).isTrue(); - assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Set.class)).isFalse(); - assertThat(conversionService.canConvert(Map.class, io.vavr.collection.List.class)).isFalse(); - - Map map = Collections.singletonMap("key", "value"); - - io.vavr.collection.Traversable result = conversionService.convert(map, io.vavr.collection.Traversable.class); - - assertThat(result).isInstanceOf(io.vavr.collection.Map.class); - } - - @Test // DATACMNS-1065 - void unwrapsVavrCollectionsToJavaOnes() { - - assertThat(unwrap(io.vavr.collection.List.of(1, 2, 3))).isInstanceOf(List.class); - assertThat(unwrap(io.vavr.collection.LinkedHashSet.of(1, 2, 3))).isInstanceOf(Set.class); - assertThat(unwrap(io.vavr.collection.LinkedHashMap.of("key", "value"))).isInstanceOf(Map.class); - } - - @Test // DATACMNS-1065 - void vavrSeqIsASupportedPageableType() { - - Set> allowedPageableTypes = QueryExecutionConverters.getAllowedPageableTypes(); - assertThat(allowedPageableTypes).contains(io.vavr.collection.Seq.class); - } - - @Test // DATAJPA-1258 - void convertsJavaListsToVavrSet() { - - List source = Collections.singletonList("foo"); - - assertThat(conversionService.convert(source, io.vavr.collection.Set.class)) // - .isInstanceOf(io.vavr.collection.Set.class); - } - @Test // DATACMNS-1299 void unwrapsPages() throws Exception { diff --git a/src/test/java/org/springframework/data/util/CustomCollectionsUnitTests.java b/src/test/java/org/springframework/data/util/CustomCollectionsUnitTests.java new file mode 100644 index 000000000..6046c0356 --- /dev/null +++ b/src/test/java/org/springframework/data/util/CustomCollectionsUnitTests.java @@ -0,0 +1,238 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util; + +import static org.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.api.bag.ImmutableBag; +import org.eclipse.collections.api.bag.MutableBag; +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.Maps; +import org.eclipse.collections.api.factory.Sets; +import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.api.list.ListIterable; +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.map.ImmutableMap; +import org.eclipse.collections.api.map.MapIterable; +import org.eclipse.collections.api.map.MutableMap; +import org.eclipse.collections.api.set.ImmutableSet; +import org.eclipse.collections.api.set.MutableSet; +import org.eclipse.collections.api.set.SetIterable; +import org.eclipse.collections.impl.map.immutable.ImmutableUnifiedMap; +import org.eclipse.collections.impl.map.mutable.UnifiedMap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; + +/** + * Unit tests for {@link CustomCollections}. + * + * @author Oliver Drotbohm + */ +@TestInstance(Lifecycle.PER_CLASS) +class CustomCollectionsUnitTests { + + ConfigurableConversionService conversionService = new DefaultConversionService(); + + @BeforeAll + void setUp() { + CustomCollections.registerConvertersIn(conversionService); + } + + @TestFactory // #1817 + Stream registersVavrCollections() { + + return new CustomCollectionTester() + .withCollections(io.vavr.collection.Seq.class, io.vavr.collection.Set.class) + .withMaps(io.vavr.collection.Map.class) + .withMapImplementations(io.vavr.collection.LinkedHashMap.class, io.vavr.collection.HashMap.class) + .verify(); + } + + @Test // DATACMNS-1065, #1817 + void conversListToVavr() { + + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Traversable.class)).isTrue(); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class)).isTrue(); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Set.class)).isTrue(); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Map.class)).isFalse(); + + List integers = Arrays.asList(1, 2, 3); + + io.vavr.collection.Traversable result = conversionService.convert(integers, + io.vavr.collection.Traversable.class); + + assertThat(result).isInstanceOf(io.vavr.collection.List.class); + } + + @Test // DATACMNS-1065, #1817 + void convertsSetToVavr() { + + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Traversable.class)).isTrue(); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Set.class)).isTrue(); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.List.class)).isTrue(); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Map.class)).isFalse(); + + Set integers = Collections.singleton(1); + + io.vavr.collection.Traversable result = conversionService.convert(integers, + io.vavr.collection.Traversable.class); + + assertThat(result).isInstanceOf(io.vavr.collection.Set.class); + } + + @Test // DATACMNS-1065, #1817 + void convertsMapToVavr() { + + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Traversable.class)).isTrue(); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Map.class)).isTrue(); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Set.class)).isFalse(); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.List.class)).isFalse(); + + Map map = Collections.singletonMap("key", "value"); + + io.vavr.collection.Traversable result = conversionService.convert(map, io.vavr.collection.Traversable.class); + + assertThat(result).isInstanceOf(io.vavr.collection.Map.class); + } + + @Test // DATACMNS-1065, #1817 + void unwrapsVavrCollectionsToJavaOnes() { + + assertThat(unwrap(io.vavr.collection.List.of(1, 2, 3))).isInstanceOf(List.class); + assertThat(unwrap(io.vavr.collection.LinkedHashSet.of(1, 2, 3))).isInstanceOf(Set.class); + assertThat(unwrap(io.vavr.collection.LinkedHashMap.of("key", "value"))).isInstanceOf(Map.class); + } + + @Test // #1817 + void rejectsInvalidMapType() { + assertThatIllegalArgumentException().isThrownBy(() -> CustomCollections.getMapBaseType(Object.class)); + } + + @Test // DATACMNS-1065, #1817 + void vavrSeqIsASupportedPaginationReturnType() { + assertThat(CustomCollections.getPaginationReturnTypes()).contains(io.vavr.collection.Seq.class); + } + + @Test // DATAJPA-1258. #1817 + void convertsJavaListsToVavrSet() { + assertThat(conversionService.convert(Collections.singletonList("foo"), io.vavr.collection.Set.class)) // + .isInstanceOf(io.vavr.collection.Set.class); + } + + private static Object unwrap(Object source) { + return CustomCollections.getUnwrappers().stream() + .reduce(source, (value, mapper) -> mapper.apply(value), (l, r) -> r); + } + + @AllArgsConstructor + static class CustomCollectionTester { + + private final Collection> expectedCollections, expectedMaps, collectionImplementations, mapImplementations; + + public CustomCollectionTester() { + + this.expectedCollections = Collections.emptyList(); + this.expectedMaps = Collections.emptyList(); + this.collectionImplementations = Collections.emptyList(); + this.mapImplementations = Collections.emptyList(); + } + + public CustomCollectionTester withCollections(Class... types) { + return new CustomCollectionTester(Arrays.asList(types), expectedMaps, collectionImplementations, + mapImplementations); + } + + public CustomCollectionTester withMaps(Class... types) { + return new CustomCollectionTester(expectedCollections, Arrays.asList(types), collectionImplementations, + mapImplementations); + } + + public CustomCollectionTester withCollectionImplementations(Class... types) { + return new CustomCollectionTester(expectedCollections, expectedMaps, Arrays.asList(types), mapImplementations); + } + + public CustomCollectionTester withMapImplementations(Class... types) { + return new CustomCollectionTester(expectedCollections, expectedMaps, collectionImplementations, + Arrays.asList(types)); + } + + public Stream verify() { + + Stream isCollection = DynamicTest.stream(expectedCollections.stream(), + it -> it.getSimpleName() + " is a collection", it -> { + assertThat(CustomCollections.isCollection(it)).isTrue(); + assertThat(CustomCollections.getCustomTypes()).contains(it); + }); + + Stream isNotMap = DynamicTest.stream(expectedCollections.stream(), + it -> it.getSimpleName() + " is not a map", it -> { + assertThat(CustomCollections.isMap(it)).isFalse(); + }); + + Stream isMap = DynamicTest.stream(expectedMaps.stream(), + it -> it.getSimpleName() + " is a map", it -> { + assertThat(CustomCollections.isMap(it)).isTrue(); + assertThat(CustomCollections.getCustomTypes()).contains(it); + }); + + Stream isNotCollection = DynamicTest.stream(expectedMaps.stream(), + it -> it.getSimpleName() + " is not a collection", it -> { + assertThat(CustomCollections.isCollection(it)).isFalse(); + }); + + Stream isMapBaseType = DynamicTest.stream(expectedMaps.stream(), + it -> it.getSimpleName() + " is a map base type", it -> { + + assertThat(CustomCollections.isMapBaseType(it)).isTrue(); + + Class expectedBaseType = Map.class.isAssignableFrom(it) ? Map.class : it; + + assertThat(CustomCollections.getMapBaseType(it)).isEqualTo(expectedBaseType); + }); + + Stream findsMapBaseType = DynamicTest.stream(mapImplementations.stream(), + it -> it.getSimpleName() + " is a map implementation type", it -> { + + if (Map.class.isAssignableFrom(it)) { + assertThat(CustomCollections.getMapBaseType(it)).isEqualTo(Map.class); + } else { + assertThat(expectedMaps).contains(CustomCollections.getMapBaseType(it)); + } + }); + + return Stream.of(isCollection, isNotMap, isMap, isNotCollection, isMapBaseType, findsMapBaseType) + .reduce(Stream::concat) + .orElse(Stream.empty()); + } + } +}