Skip to content

Commit b3ec06f

Browse files
committed
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.
1 parent 87c5e86 commit b3ec06f

File tree

10 files changed

+838
-430
lines changed

10 files changed

+838
-430
lines changed

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

+4-14
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,14 @@
1616
package org.springframework.data.convert;
1717

1818
import java.lang.annotation.Annotation;
19-
import java.util.ArrayList;
20-
import java.util.Arrays;
21-
import java.util.Collection;
22-
import java.util.Collections;
23-
import java.util.HashSet;
24-
import java.util.LinkedHashSet;
25-
import java.util.List;
26-
import java.util.Map;
27-
import java.util.Optional;
28-
import java.util.Set;
19+
import java.util.*;
2920
import java.util.concurrent.ConcurrentHashMap;
3021
import java.util.function.Function;
3122
import java.util.function.Predicate;
3223
import java.util.stream.Collectors;
3324

3425
import org.apache.commons.logging.Log;
3526
import org.apache.commons.logging.LogFactory;
36-
3727
import org.springframework.core.GenericTypeResolver;
3828
import org.springframework.core.annotation.AnnotationUtils;
3929
import org.springframework.core.convert.converter.Converter;
@@ -45,9 +35,9 @@
4535
import org.springframework.data.convert.ConverterBuilder.ConverterAware;
4636
import org.springframework.data.mapping.PersistentProperty;
4737
import org.springframework.data.mapping.model.SimpleTypeHolder;
38+
import org.springframework.data.util.CustomCollections;
4839
import org.springframework.data.util.Predicates;
4940
import org.springframework.data.util.Streamable;
50-
import org.springframework.data.util.VavrCollectionConverters;
5141
import org.springframework.lang.NonNull;
5242
import org.springframework.lang.Nullable;
5343
import org.springframework.util.Assert;
@@ -214,7 +204,7 @@ public void registerConvertersIn(@NonNull ConverterRegistry conversionService) {
214204
Assert.notNull(conversionService, "ConversionService must not be null");
215205

216206
converters.forEach(it -> registerConverterIn(it, conversionService));
217-
VavrCollectionConverters.getConvertersToRegister().forEach(it -> registerConverterIn(it, conversionService));
207+
CustomCollections.registerConvertersIn(conversionService);
218208
}
219209

220210
/**
@@ -876,7 +866,7 @@ Collection<?> getStoreConverters() {
876866
}
877867

878868
@Override
879-
public boolean equals(Object o) {
869+
public boolean equals(@Nullable Object o) {
880870

881871
if (this == o) {
882872
return true;

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

+16-44
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.concurrent.CompletableFuture;
2727
import java.util.concurrent.ConcurrentHashMap;
2828
import java.util.concurrent.Future;
29+
import java.util.function.Function;
2930
import java.util.stream.Stream;
3031

3132
import org.springframework.core.convert.ConversionService;
@@ -38,12 +39,13 @@
3839
import org.springframework.data.domain.Page;
3940
import org.springframework.data.domain.Slice;
4041
import org.springframework.data.geo.GeoResults;
42+
import org.springframework.data.util.CustomCollections;
4143
import org.springframework.data.util.NullableWrapper;
4244
import org.springframework.data.util.NullableWrapperConverters;
4345
import org.springframework.data.util.StreamUtils;
4446
import org.springframework.data.util.Streamable;
4547
import org.springframework.data.util.TypeInformation;
46-
import org.springframework.data.util.VavrCollectionConverters;
48+
import org.springframework.lang.NonNull;
4749
import org.springframework.lang.Nullable;
4850
import org.springframework.scheduling.annotation.AsyncResult;
4951
import org.springframework.util.Assert;
@@ -80,7 +82,7 @@ public abstract class QueryExecutionConverters {
8082

8183
private static final Set<WrapperType> WRAPPER_TYPES = new HashSet<>();
8284
private static final Set<WrapperType> UNWRAPPER_TYPES = new HashSet<WrapperType>();
83-
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<>();
85+
private static final Set<Function<Object, Object>> UNWRAPPERS = new HashSet<>();
8486
private static final Set<Class<?>> ALLOWED_PAGEABLE_TYPES = new HashSet<>();
8587
private static final Map<Class<?>, ExecutionAdapter> EXECUTION_ADAPTER = new HashMap<>();
8688
private static final Map<Class<?>, Boolean> supportsCache = new ConcurrentReferenceHashMap<>();
@@ -98,16 +100,19 @@ public abstract class QueryExecutionConverters {
98100

99101
WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());
100102

101-
if (VAVR_PRESENT) {
103+
UNWRAPPERS.addAll(CustomCollections.getUnwrappers());
104+
105+
CustomCollections.getCustomTypes().stream()
106+
.map(WrapperType::multiValue)
107+
.forEach(WRAPPER_TYPES::add);
102108

103-
WRAPPER_TYPES.add(VavrTraversableUnwrapper.INSTANCE.getWrapperType());
104-
UNWRAPPERS.add(VavrTraversableUnwrapper.INSTANCE);
109+
CustomCollections.getPaginationReturnTypes().forEach(ALLOWED_PAGEABLE_TYPES::add);
110+
111+
if (VAVR_PRESENT) {
105112

106113
// Try support
107114
WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class));
108115
EXECUTION_ADAPTER.put(io.vavr.control.Try.class, it -> io.vavr.control.Try.of(it::get));
109-
110-
ALLOWED_PAGEABLE_TYPES.add(io.vavr.collection.Seq.class);
111116
}
112117
}
113118

@@ -195,10 +200,7 @@ public static void registerConvertersIn(ConfigurableConversionService conversion
195200
conversionService.removeConvertible(Collection.class, Object.class);
196201

197202
NullableWrapperConverters.registerConvertersIn(conversionService);
198-
199-
if (VAVR_PRESENT) {
200-
conversionService.addConverter(VavrCollectionConverters.FromJavaConverter.INSTANCE);
201-
}
203+
CustomCollections.registerConvertersIn(conversionService);
202204

203205
conversionService.addConverter(new NullableWrapperToCompletableFutureConverter());
204206
conversionService.addConverter(new NullableWrapperToFutureConverter());
@@ -220,9 +222,9 @@ public static Object unwrap(@Nullable Object source) {
220222
return source;
221223
}
222224

223-
for (Converter<Object, Object> converter : UNWRAPPERS) {
225+
for (Function<Object, Object> converter : UNWRAPPERS) {
224226

225-
Object result = converter.convert(source);
227+
Object result = converter.apply(source);
226228

227229
if (result != source) {
228230
return result;
@@ -382,36 +384,6 @@ static WrapperType getWrapperType() {
382384
}
383385
}
384386

385-
/**
386-
* Converter to unwrap Vavr {@link io.vavr.collection.Traversable} instances.
387-
*
388-
* @author Oliver Gierke
389-
* @since 2.0
390-
*/
391-
private enum VavrTraversableUnwrapper implements Converter<Object, Object> {
392-
393-
INSTANCE;
394-
395-
private static final TypeDescriptor OBJECT_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
396-
397-
@Nullable
398-
@Override
399-
@SuppressWarnings("null")
400-
public Object convert(Object source) {
401-
402-
if (source instanceof io.vavr.collection.Traversable) {
403-
return VavrCollectionConverters.ToJavaConverter.INSTANCE //
404-
.convert(source, TypeDescriptor.forObject(source), OBJECT_DESCRIPTOR);
405-
}
406-
407-
return source;
408-
}
409-
410-
public WrapperType getWrapperType() {
411-
return WrapperType.multiValue(io.vavr.collection.Traversable.class);
412-
}
413-
}
414-
415387
private static class IterableToStreamableConverter implements ConditionalGenericConverter {
416388

417389
private static final TypeDescriptor STREAMABLE = TypeDescriptor.valueOf(Streamable.class);
@@ -477,7 +449,7 @@ public Cardinality getCardinality() {
477449
}
478450

479451
@Override
480-
public boolean equals(Object o) {
452+
public boolean equals(@Nullable Object o) {
481453

482454
if (this == o) {
483455
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.util;
17+
18+
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Set;
22+
import java.util.function.Function;
23+
24+
import org.springframework.core.convert.converter.ConverterRegistry;
25+
26+
/**
27+
* An SPI to register custom collection types. Implementations need to be registered via
28+
* {@code META-INF/spring.factories}.
29+
*
30+
* @author Oliver Drotbohm
31+
* @since 2.7
32+
*/
33+
public interface CustomCollectionRegistrar {
34+
35+
/**
36+
* Returns whether the registrar is available, meaning whether it can be used at runtime. Primary use is for
37+
* implementations that need to perform a classpath check to prevent the actual methods loading classes that might not
38+
* be available.
39+
*
40+
* @return whether the registrar is available
41+
*/
42+
default boolean isAvailable() {
43+
return true;
44+
}
45+
46+
/**
47+
* Returns all types that are supposed to be considered maps. Primary requirement is key and value generics expressed
48+
* in the first and second generics parameter of the type. Also, the types should be transformable into their
49+
* Java-native equivalent using {@link #toJavaNativeCollection()}.
50+
*
51+
* @return will never be {@literal null}.
52+
* @see #toJavaNativeCollection()
53+
*/
54+
Collection<Class<?>> getMapTypes();
55+
56+
/**
57+
* Returns all types that are supposed to be considered collections. Primary requirement is that their component types
58+
* are expressed as first generics parameter. Also, the types should be transformable into their Java-native
59+
* equivalent using {@link #toJavaNativeCollection()}.
60+
*
61+
* @return will never be {@literal null}.
62+
* @see #toJavaNativeCollection()
63+
*/
64+
Collection<Class<?>> getCollectionTypes();
65+
66+
/**
67+
* Return all types that are considered valid return types for methods using pagination. These are usually collections
68+
* with a stable order, like {@link List} but no {@link Set}s, as pagination usually involves sorting.
69+
*
70+
* @return will never be {@literal null}.
71+
*/
72+
default Collection<Class<?>> getAllowedPaginationReturnTypes() {
73+
return Collections.emptyList();
74+
}
75+
76+
/**
77+
* Register all converters to convert instances of the types returned by {@link #getCollectionTypes()} and
78+
* {@link #getMapTypes()} from an to their Java-native counterparts.
79+
*
80+
* @param registry will never be {@literal null}.
81+
*/
82+
void registerConvertersIn(ConverterRegistry registry);
83+
84+
/**
85+
* Returns a {@link Function} to convert instances of their Java-native counterpart.
86+
*
87+
* @return must not be {@literal null}.
88+
*/
89+
Function<Object, Object> toJavaNativeCollection();
90+
}

0 commit comments

Comments
 (0)