Browse Source

Improve custom collection support.

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.
pull/2772/head
Oliver Drotbohm 4 years ago
parent
commit
6ebe288f83
No known key found for this signature in database
GPG Key ID: C25FBFA0DA493A1D
  1. 18
      src/main/java/org/springframework/data/convert/CustomConversions.java
  2. 67
      src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java
  3. 90
      src/main/java/org/springframework/data/util/CustomCollectionRegistrar.java
  4. 491
      src/main/java/org/springframework/data/util/CustomCollections.java
  5. 10
      src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java
  6. 127
      src/main/java/org/springframework/data/util/TypeDiscoverer.java
  7. 205
      src/main/java/org/springframework/data/util/VavrCollectionConverters.java
  8. 1
      src/main/resources/META-INF/spring.factories
  9. 73
      src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java
  10. 238
      src/test/java/org/springframework/data/util/CustomCollectionsUnitTests.java

18
src/main/java/org/springframework/data/convert/CustomConversions.java

@ -16,16 +16,7 @@ @@ -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; @@ -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 @@ -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 { @@ -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 { @@ -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;

67
src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java

@ -26,6 +26,7 @@ import java.util.Set; @@ -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; @@ -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 { @@ -80,7 +82,7 @@ public abstract class QueryExecutionConverters {
private static final Set<WrapperType> WRAPPER_TYPES = new HashSet<>();
private static final Set<WrapperType> UNWRAPPER_TYPES = new HashSet<WrapperType>();
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<>();
private static final Set<Function<Object, Object>> UNWRAPPERS = new HashSet<>();
private static final Set<Class<?>> ALLOWED_PAGEABLE_TYPES = new HashSet<>();
private static final Map<Class<?>, ExecutionAdapter> EXECUTION_ADAPTER = new HashMap<>();
private static final Map<Class<?>, Boolean> supportsCache = new ConcurrentReferenceHashMap<>();
@ -98,16 +100,19 @@ public abstract class QueryExecutionConverters { @@ -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 { @@ -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 { @@ -220,9 +222,9 @@ public abstract class QueryExecutionConverters {
return source;
}
for (Converter<Object, Object> converter : UNWRAPPERS) {
for (Function<Object, Object> 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 { @@ -310,7 +312,7 @@ public abstract class QueryExecutionConverters {
* (non-Javadoc)
* @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
*/
@NonNull
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
@ -399,40 +401,6 @@ public abstract class QueryExecutionConverters { @@ -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<Object, Object> {
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 { @@ -446,6 +414,7 @@ public abstract class QueryExecutionConverters {
* (non-Javadoc)
* @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
*/
@NonNull
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Iterable.class, Object.class));
@ -514,7 +483,7 @@ public abstract class QueryExecutionConverters { @@ -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;

90
src/main/java/org/springframework/data/util/CustomCollectionRegistrar.java

@ -0,0 +1,90 @@ @@ -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<Class<?>> 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<Class<?>> 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<Class<?>> 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<Object, Object> toJavaNativeCollection();
}

491
src/main/java/org/springframework/data/util/CustomCollections.java

@ -0,0 +1,491 @@ @@ -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<Class<?>> CUSTOM_TYPES, CUSTOM_MAP_TYPES, CUSTOM_COLLECTION_TYPES, PAGINATION_RETURN_TYPES;
private static final Set<Class<?>> 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<CustomCollectionRegistrar> 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<Class<?>> getCustomTypes() {
return CUSTOM_TYPES;
}
/**
* Returns all types that are allowed pagination return types.
*
* @return will never be {@literal null}.
*/
public static Set<Class<?>> 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<Function<Object, Object>> 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<?>, Class<?>> EQUALS = (left, right) -> left.equals(right);
private static final BiPredicate<Class<?>, Class<?>> IS_ASSIGNABLE = (left, right) -> left.isAssignableFrom(right);
private static final Function<Class<?>, Boolean> IS_NOT_NULL = it -> it != null;
private final Collection<Class<?>> types;
public SearchableTypes(Set<Class<?>> types, Class<?>... additional) {
List<Class<?>> 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<String> 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 <T>
* @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> T isOneOf(Class<?> type, BiPredicate<Class<?>, Class<?>> matcher, Function<Class<?>, 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<?>, Class<?>> rejectNull(Supplier<String> 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<Class<?>> getMapTypes() {
return Collections.singleton(io.vavr.collection.Map.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#getCollectionTypes()
*/
@Override
public Collection<Class<?>> getCollectionTypes() {
return Arrays.asList(Seq.class, io.vavr.collection.Set.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#getAllowedPaginationReturnTypes()
*/
@Override
public Collection<Class<?>> 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<Object, Object> 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<ConvertiblePair> 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<ConvertiblePair> CONVERTIBLE_PAIRS;
static {
Set<ConvertiblePair> 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<ConvertiblePair> 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;
}
}
}
}

10
src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java

@ -20,7 +20,6 @@ import java.lang.reflect.Type; @@ -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<T> extends ParentTypeAwareTypeInformation<T> @@ -134,7 +133,8 @@ class ParameterizedTypeInformation<T> extends ParentTypeAwareTypeInformation<T>
: target.getSuperTypeInformation(rawType);
List<TypeInformation<?>> myParameters = getTypeArguments();
List<TypeInformation<?>> typeParameters = otherTypeInformation == null ? Collections.emptyList()
List<TypeInformation<?>> typeParameters = otherTypeInformation == null //
? java.util.Collections.emptyList() //
: otherTypeInformation.getTypeArguments();
if (myParameters.size() != typeParameters.size()) {
@ -158,8 +158,10 @@ class ParameterizedTypeInformation<T> extends ParentTypeAwareTypeInformation<T> @@ -158,8 +158,10 @@ class ParameterizedTypeInformation<T> extends ParentTypeAwareTypeInformation<T>
@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]);
}

127
src/main/java/org/springframework/data/util/TypeDiscoverer.java

@ -25,7 +25,12 @@ import java.lang.reflect.ParameterizedType; @@ -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; @@ -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; @@ -50,36 +54,6 @@ import org.springframework.util.ReflectionUtils;
*/
class TypeDiscoverer<S> implements TypeInformation<S> {
protected static final Class<?>[] MAP_TYPES;
private static final Class<?>[] COLLECTION_TYPES;
static {
ClassLoader classLoader = TypeDiscoverer.class.getClassLoader();
Set<Class<?>> 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<Class<?>> 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<TypeVariable<?>, Type> typeVariableMap;
private final Map<String, Optional<TypeInformation<?>>> fieldTypes = new ConcurrentHashMap<>();
@ -343,16 +317,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> { @@ -343,16 +317,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
* @see org.springframework.data.util.TypeInformation#isMap()
*/
public boolean isMap() {
Class<S> 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<S> implements TypeInformation<S> { @@ -366,7 +331,9 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
@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<S> implements TypeInformation<S> { @@ -381,7 +348,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
return rawType.isArray() //
|| Iterable.class.equals(rawType) //
|| Streamable.class.isAssignableFrom(rawType) //
|| isCollection();
|| CustomCollections.isCollection(rawType);
}
/*
@ -403,7 +370,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> { @@ -403,7 +370,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
}
if (isMap()) {
return getTypeArgument(getMapBaseType(), 0);
return getTypeArgument(CustomCollections.getMapBaseType(rawType), 0);
}
if (Iterable.class.isAssignableFrom(rawType)) {
@ -492,7 +459,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> { @@ -492,7 +459,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
* @see org.springframework.data.util.TypeInformation#getTypeParameters()
*/
public List<TypeInformation<?>> getTypeArguments() {
return Collections.emptyList();
return java.util.Collections.emptyList();
}
/* (non-Javadoc)
@ -538,14 +505,6 @@ class TypeDiscoverer<S> implements TypeInformation<S> { @@ -538,14 +505,6 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
: 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<S> implements TypeInformation<S> { @@ -587,66 +546,6 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
return hashCode;
}
/**
* Returns whether the current type is considered a collection.
*
* @return
*/
private boolean isCollection() {
Class<S> 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<S> 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<S> 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());
}

205
src/main/java/org/springframework/data/util/VavrCollectionConverters.java

@ -1,205 +0,0 @@ @@ -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<Object> 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<Class<?>> 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<ConvertiblePair> 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<ConvertiblePair> 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<ConvertiblePair> CONVERTIBLE_PAIRS;
static {
Set<ConvertiblePair> 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);
}
}
}

1
src/main/resources/META-INF/spring.factories

@ -1 +1,2 @@ @@ -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

73
src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java

@ -32,10 +32,8 @@ import scala.Option; @@ -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 { @@ -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<Integer> 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<Integer> 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<String, String> 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<Class<?>> allowedPageableTypes = QueryExecutionConverters.getAllowedPageableTypes();
assertThat(allowedPageableTypes).contains(io.vavr.collection.Seq.class);
}
@Test // DATAJPA-1258
void convertsJavaListsToVavrSet() {
List<String> 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 {

238
src/test/java/org/springframework/data/util/CustomCollectionsUnitTests.java

@ -0,0 +1,238 @@ @@ -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<DynamicTest> 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<Integer> 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<Integer> 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<String, String> 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<Class<?>> 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<DynamicTest> verify() {
Stream<DynamicTest> isCollection = DynamicTest.stream(expectedCollections.stream(),
it -> it.getSimpleName() + " is a collection", it -> {
assertThat(CustomCollections.isCollection(it)).isTrue();
assertThat(CustomCollections.getCustomTypes()).contains(it);
});
Stream<DynamicTest> isNotMap = DynamicTest.stream(expectedCollections.stream(),
it -> it.getSimpleName() + " is not a map", it -> {
assertThat(CustomCollections.isMap(it)).isFalse();
});
Stream<DynamicTest> isMap = DynamicTest.stream(expectedMaps.stream(),
it -> it.getSimpleName() + " is a map", it -> {
assertThat(CustomCollections.isMap(it)).isTrue();
assertThat(CustomCollections.getCustomTypes()).contains(it);
});
Stream<DynamicTest> isNotCollection = DynamicTest.stream(expectedMaps.stream(),
it -> it.getSimpleName() + " is not a collection", it -> {
assertThat(CustomCollections.isCollection(it)).isFalse();
});
Stream<DynamicTest> 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<DynamicTest> 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());
}
}
}
Loading…
Cancel
Save