Support for Eclipse Collections.

Using the newly introduced CustomCollectionsRegistrar SPI to provide support for Eclipse Collections (Imm|M)utable(List|Set|Bag|Map).

Fixes #2618.
This commit is contained in:
Oliver Drotbohm
2022-05-03 13:32:52 +02:00
parent 6ebe288f83
commit 90fb5c7f46
5 changed files with 365 additions and 4 deletions

17
pom.xml
View File

@@ -18,6 +18,7 @@
<properties>
<javaslang>2.0.6</javaslang>
<vavr>0.10.4</vavr>
<eclipse-collections>11.0.0</eclipse-collections>
<scala>2.11.7</scala>
<xmlbeam>1.4.23</xmlbeam>
@@ -223,6 +224,22 @@
<version>${vavr}</version>
<optional>true</optional>
</dependency>
<!-- Eclipse Collections -->
<dependency>
<groupId>org.eclipse.collections</groupId>
<artifactId>eclipse-collections-api</artifactId>
<version>${eclipse-collections}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.collections</groupId>
<artifactId>eclipse-collections</artifactId>
<version>${eclipse-collections}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.el</groupId>

View File

@@ -34,8 +34,24 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
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.Bags;
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.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.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.NonNull;
@@ -488,4 +504,232 @@ public class CustomCollections {
}
}
}
static class EclipseCollections implements CustomCollectionRegistrar {
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#isAvailable()
*/
@Override
public boolean isAvailable() {
return ClassUtils.isPresent("org.eclipse.collections.api.list.ImmutableList",
EclipseCollections.class.getClassLoader());
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#getCollectionTypes()
*/
@Override
public Collection<Class<?>> getCollectionTypes() {
return Arrays.asList(ImmutableList.class, ImmutableSet.class, ImmutableBag.class, //
MutableList.class, MutableSet.class, MutableBag.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#getMapTypes()
*/
@Override
public Collection<Class<?>> getMapTypes() {
return Arrays.asList(ImmutableMap.class, MutableMap.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#getAllowedPaginationReturnTypes()
*/
@Override
public Collection<Class<?>> getAllowedPaginationReturnTypes() {
return Arrays.asList(ImmutableList.class, MutableList.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#toJavaNativeCollection()
*/
@Override
public Function<Object, Object> toJavaNativeCollection() {
return source -> source instanceof RichIterable
? EclipseToJavaConverter.INSTANCE.convert(source)
: source;
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.CustomCollectionRegistrar#registerConvertersIn(org.springframework.core.convert.converter.ConverterRegistry)
*/
@Override
public void registerConvertersIn(ConverterRegistry registry) {
registry.addConverter(EclipseToJavaConverter.INSTANCE);
registry.addConverter(JavaToEclipseConverter.INSTANCE);
}
enum EclipseToJavaConverter implements Converter<Object, Object>, ConditionalConverter {
INSTANCE;
private static final TypeDescriptor RICH_ITERABLE_DESCRIPTOR = TypeDescriptor.valueOf(RichIterable.class);
/*
* (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(RICH_ITERABLE_DESCRIPTOR)
&& COLLECTIONS_AND_MAP.contains(targetType.getType());
}
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@Override
public Object convert(@Nullable Object source) {
if (source instanceof ImmutableList) {
return ((ImmutableList<?>) source).toList();
}
if (source instanceof ImmutableBag) {
return ((ImmutableBag<?>) source).toList();
}
if (source instanceof ImmutableSet) {
return ((ImmutableSet<?>) source).toSet();
}
if (source instanceof ImmutableMap) {
return ((ImmutableMap<?, ?>) source).toMap();
}
return source;
}
}
enum JavaToEclipseConverter implements ConditionalGenericConverter {
INSTANCE;
private static final Set<ConvertiblePair> CONVERTIBLE_PAIRS;
static {
Set<ConvertiblePair> pairs = new HashSet<>();
pairs.add(new ConvertiblePair(Collection.class, RichIterable.class));
pairs.add(new ConvertiblePair(Set.class, MutableSet.class));
pairs.add(new ConvertiblePair(Set.class, MutableList.class));
pairs.add(new ConvertiblePair(Set.class, ImmutableSet.class));
pairs.add(new ConvertiblePair(Set.class, ImmutableList.class));
pairs.add(new ConvertiblePair(List.class, MutableList.class));
pairs.add(new ConvertiblePair(List.class, ImmutableList.class));
pairs.add(new ConvertiblePair(Map.class, RichIterable.class));
pairs.add(new ConvertiblePair(Map.class, MutableMap.class));
pairs.add(new ConvertiblePair(Map.class, ImmutableMap.class));
CONVERTIBLE_PAIRS = Collections.unmodifiableSet(pairs);
}
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
*/
@NonNull
@Override
public 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() && MapIterable.class.isAssignableFrom(targetType.getType())) {
return false;
}
// Prevent maps to be mapped to collections
if (sourceType.isMap() //
&& !(MapIterable.class.isAssignableFrom(targetType.getType())
|| targetType.getType().equals(RichIterable.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 (ImmutableList.class.isAssignableFrom(targetType)) {
return Lists.immutable.ofAll((Iterable<?>) source);
}
if (ImmutableSet.class.isAssignableFrom(targetType)) {
return Sets.immutable.ofAll((Iterable<?>) source);
}
if (ImmutableBag.class.isAssignableFrom(targetType)) {
return Bags.immutable.ofAll((Iterable<?>) source);
}
if (ImmutableMap.class.isAssignableFrom(targetType)) {
return Maps.immutable.ofAll((Map<?, ?>) source);
}
if (MutableList.class.isAssignableFrom(targetType)) {
return Lists.mutable.ofAll((Iterable<?>) source);
}
if (MutableSet.class.isAssignableFrom(targetType)) {
return Sets.mutable.ofAll((Iterable<?>) source);
}
if (MutableBag.class.isAssignableFrom(targetType)) {
return Bags.mutable.ofAll((Iterable<?>) source);
}
if (MutableMap.class.isAssignableFrom(targetType)) {
return Maps.mutable.ofMap((Map<?, ?>) source);
}
// No dedicated type asked for, probably RichIterable.
// Try to stay as close to the source value.
if (source instanceof List) {
return Lists.mutable.ofAll((Iterable<?>) source);
}
if (source instanceof Set) {
return Sets.mutable.ofAll((Iterable<?>) source);
}
if (source instanceof Map) {
return Maps.mutable.ofMap((Map<?, ?>) source);
}
return source;
}
}
}
}

View File

@@ -1,2 +1,3 @@
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration
org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections
org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections, \
org.springframework.data.util.CustomCollections.EclipseCollections

View File

@@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import io.vavr.collection.Seq;
import io.vavr.control.Option;
import reactor.core.publisher.Mono;
import java.io.Serializable;
import java.lang.reflect.Method;
@@ -27,9 +28,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.stream.Stream;
import org.eclipse.collections.api.list.ImmutableList;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
@@ -249,6 +249,15 @@ class QueryMethodUnitTests {
assertThat(returnedType.getDomainType()).isEqualTo(User.class);
}
@Test // #1817
void considersEclipseCollectionCollectionQuery() throws Exception {
Method method = SampleRepository.class.getMethod("returnsEclipseCollection");
QueryMethod queryMethod = new QueryMethod(method, metadata, factory);
assertThat(queryMethod.isCollectionQuery()).isTrue();
}
interface SampleRepository extends Repository<User, Serializable> {
String pagingMethodWithInvalidReturnType(Pageable pageable);
@@ -295,6 +304,8 @@ class QueryMethodUnitTests {
Future<Option<User>> returnsFutureOfOption();
Mono<Slice<User>> reactiveSlice();
ImmutableList<User> returnsEclipseCollection();
}
class User {

View File

@@ -78,6 +78,17 @@ class CustomCollectionsUnitTests {
.verify();
}
@TestFactory // #1817
Stream<DynamicTest> registersEclipseCollections() {
return new CustomCollectionTester()
.withCollections(ImmutableSet.class, ImmutableList.class, ImmutableBag.class, MutableSet.class,
MutableList.class, MutableBag.class)
.withMaps(ImmutableMap.class, MutableMap.class)
.withMapImplementations(ImmutableUnifiedMap.class, UnifiedMap.class)
.verify();
}
@Test // DATACMNS-1065, #1817
void conversListToVavr() {
@@ -125,6 +136,63 @@ class CustomCollectionsUnitTests {
assertThat(result).isInstanceOf(io.vavr.collection.Map.class);
}
@Test // #1817
void conversListToEclipse() {
assertThat(conversionService.canConvert(List.class, RichIterable.class)).isTrue();
assertThat(conversionService.canConvert(List.class, ImmutableList.class)).isTrue();
assertThat(conversionService.canConvert(List.class, ImmutableSet.class)).isTrue();
assertThat(conversionService.canConvert(List.class, ImmutableBag.class)).isTrue();
assertThat(conversionService.canConvert(List.class, ImmutableMap.class)).isFalse();
List<Integer> integers = Arrays.asList(1, 2, 3);
assertThat(conversionService.convert(integers, RichIterable.class)).isInstanceOf(MutableList.class);
assertThat(conversionService.convert(integers, MutableList.class)).isInstanceOf(MutableList.class);
assertThat(conversionService.convert(integers, MutableSet.class)).isInstanceOf(MutableSet.class);
assertThat(conversionService.convert(integers, MutableBag.class)).isInstanceOf(MutableBag.class);
assertThat(conversionService.convert(integers, ImmutableList.class)).isInstanceOf(ImmutableList.class);
assertThat(conversionService.convert(integers, ImmutableSet.class)).isInstanceOf(ImmutableSet.class);
assertThat(conversionService.convert(integers, ImmutableBag.class)).isInstanceOf(ImmutableBag.class);
}
@Test // #1817
void convertsSetToEclipse() {
assertThat(conversionService.canConvert(Set.class, ImmutableSet.class)).isTrue();
assertThat(conversionService.canConvert(Set.class, ImmutableBag.class)).isTrue();
assertThat(conversionService.canConvert(Set.class, ImmutableList.class)).isTrue();
assertThat(conversionService.canConvert(Set.class, ImmutableMap.class)).isFalse();
Set<Integer> integers = Collections.singleton(1);
assertThat(conversionService.convert(integers, RichIterable.class)).isInstanceOf(MutableSet.class);
assertThat(conversionService.convert(integers, MutableList.class)).isInstanceOf(MutableList.class);
assertThat(conversionService.convert(integers, MutableSet.class)).isInstanceOf(MutableSet.class);
assertThat(conversionService.convert(integers, MutableBag.class)).isInstanceOf(MutableBag.class);
assertThat(conversionService.convert(integers, ImmutableList.class)).isInstanceOf(ImmutableList.class);
assertThat(conversionService.convert(integers, ImmutableSet.class)).isInstanceOf(ImmutableSet.class);
assertThat(conversionService.convert(integers, ImmutableBag.class)).isInstanceOf(ImmutableBag.class);
}
@Test // #1817
void convertsMapToEclipse() {
assertThat(conversionService.canConvert(Map.class, RichIterable.class)).isTrue();
assertThat(conversionService.canConvert(Map.class, MapIterable.class)).isTrue();
assertThat(conversionService.canConvert(Map.class, SetIterable.class)).isFalse();
assertThat(conversionService.canConvert(Map.class, ListIterable.class)).isFalse();
Map<String, String> map = Collections.singletonMap("key", "value");
assertThat(conversionService.convert(map, RichIterable.class)).isInstanceOf(MutableMap.class);
assertThat(conversionService.convert(map, ImmutableMap.class)).isInstanceOf(ImmutableMap.class);
// Required as MutableMap implements both Iterable and Map and for the standard
// Java compiler this creates an ambiguity
assertThat((Map<?, ?>) conversionService.convert(map, MutableMap.class)).isInstanceOf(MutableMap.class);
}
@Test // DATACMNS-1065, #1817
void unwrapsVavrCollectionsToJavaOnes() {
@@ -133,6 +201,18 @@ class CustomCollectionsUnitTests {
assertThat(unwrap(io.vavr.collection.LinkedHashMap.of("key", "value"))).isInstanceOf(Map.class);
}
@Test // #1817
void unwrapsEclipseCollectionsToJavaOnes() {
assertThat(unwrap(Lists.immutable.of(1, 2, 3))).isInstanceOf(List.class);
assertThat(unwrap(Sets.immutable.of(1, 2, 3))).isInstanceOf(Set.class);
assertThat(unwrap(Maps.immutable.of("key", "value"))).isInstanceOf(Map.class);
assertThat(unwrap(Lists.mutable.of(1, 2, 3))).isInstanceOf(List.class);
assertThat(unwrap(Sets.mutable.of(1, 2, 3))).isInstanceOf(Set.class);
assertThat(unwrap(Maps.mutable.of("key", "value"))).isInstanceOf(Map.class);
}
@Test // #1817
void rejectsInvalidMapType() {
assertThatIllegalArgumentException().isThrownBy(() -> CustomCollections.getMapBaseType(Object.class));
@@ -143,7 +223,15 @@ class CustomCollectionsUnitTests {
assertThat(CustomCollections.getPaginationReturnTypes()).contains(io.vavr.collection.Seq.class);
}
@Test // DATAJPA-1258. #1817
@TestFactory // #1817
Stream<DynamicTest> eclipseSupportedPaginationReturnTypes() {
return DynamicTest.stream(Stream.of(ImmutableList.class, MutableList.class),
it -> it.getSimpleName() + " is a pagination return type",
it -> assertThat(CustomCollections.getPaginationReturnTypes()).contains(it));
}
@Test // DATAJPA-1258, #1817
void convertsJavaListsToVavrSet() {
assertThat(conversionService.convert(Collections.singletonList("foo"), io.vavr.collection.Set.class)) //
.isInstanceOf(io.vavr.collection.Set.class);