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()); + } + } +}