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.
This commit is contained in:
Oliver Drotbohm
2022-05-02 23:23:52 +02:00
parent 87c5e866ca
commit b3ec06f479
10 changed files with 838 additions and 430 deletions

View File

@@ -31,10 +31,8 @@ import scala.Option;
import java.io.IOException;
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;
@@ -182,77 +180,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();
var 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();
var 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();
var 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() {
var allowedPageableTypes = QueryExecutionConverters.getAllowedPageableTypes();
assertThat(allowedPageableTypes).contains(io.vavr.collection.Seq.class);
}
@Test // DATAJPA-1258
void convertsJavaListsToVavrSet() {
var 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 {

View File

@@ -0,0 +1,219 @@
/*
* 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.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();
var integers = Arrays.asList(1, 2, 3);
var 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();
var integers = Collections.singleton(1);
var 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();
var map = Collections.singletonMap("key", "value");
var 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() {
var isCollection = DynamicTest.stream(expectedCollections.stream(),
it -> it.getSimpleName() + " is a collection", it -> {
assertThat(CustomCollections.isCollection(it)).isTrue();
assertThat(CustomCollections.getCustomTypes()).contains(it);
});
var isNotMap = DynamicTest.stream(expectedCollections.stream(),
it -> it.getSimpleName() + " is not a map", it -> {
assertThat(CustomCollections.isMap(it)).isFalse();
});
var isMap = DynamicTest.stream(expectedMaps.stream(),
it -> it.getSimpleName() + " is a map", it -> {
assertThat(CustomCollections.isMap(it)).isTrue();
assertThat(CustomCollections.getCustomTypes()).contains(it);
});
var isNotCollection = DynamicTest.stream(expectedMaps.stream(),
it -> it.getSimpleName() + " is not a collection", it -> {
assertThat(CustomCollections.isCollection(it)).isFalse();
});
var 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);
});
var 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());
}
}
}