Browse Source
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
10 changed files with 861 additions and 459 deletions
@ -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(); |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -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 +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 |
||||
|
||||
@ -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…
Reference in new issue