Skip to content

Commit 6ebe288

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 b7e1b37 commit 6ebe288

File tree

10 files changed

+861
-459
lines changed

10 files changed

+861
-459
lines changed

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

Lines changed: 4 additions & 14 deletions
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;
@@ -44,9 +34,9 @@
4434
import org.springframework.core.convert.support.GenericConversionService;
4535
import org.springframework.data.convert.ConverterBuilder.ConverterAware;
4636
import org.springframework.data.mapping.model.SimpleTypeHolder;
37+
import org.springframework.data.util.CustomCollections;
4738
import org.springframework.data.util.Predicates;
4839
import org.springframework.data.util.Streamable;
49-
import org.springframework.data.util.VavrCollectionConverters;
5040
import org.springframework.lang.Nullable;
5141
import org.springframework.util.Assert;
5242
import org.springframework.util.ObjectUtils;
@@ -182,7 +172,7 @@ public void registerConvertersIn(ConverterRegistry conversionService) {
182172
Assert.notNull(conversionService, "ConversionService must not be null!");
183173

184174
converters.forEach(it -> registerConverterIn(it, conversionService));
185-
VavrCollectionConverters.getConvertersToRegister().forEach(it -> registerConverterIn(it, conversionService));
175+
CustomCollections.registerConvertersIn(conversionService);
186176
}
187177

188178
@Nullable
@@ -844,7 +834,7 @@ Collection<?> getStoreConverters() {
844834
* @see java.lang.Object#equals(java.lang.Object)
845835
*/
846836
@Override
847-
public boolean equals(Object o) {
837+
public boolean equals(@Nullable Object o) {
848838

849839
if (this == o) {
850840
return true;

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

Lines changed: 18 additions & 49 deletions
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;
@@ -310,7 +312,7 @@ private static abstract class AbstractWrapperTypeConverter implements GenericCon
310312
* (non-Javadoc)
311313
* @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
312314
*/
313-
315+
@NonNull
314316
@Override
315317
public Set<ConvertiblePair> getConvertibleTypes() {
316318

@@ -399,40 +401,6 @@ static WrapperType getWrapperType() {
399401
}
400402
}
401403

402-
/**
403-
* Converter to unwrap Vavr {@link io.vavr.collection.Traversable} instances.
404-
*
405-
* @author Oliver Gierke
406-
* @since 2.0
407-
*/
408-
private enum VavrTraversableUnwrapper implements Converter<Object, Object> {
409-
410-
INSTANCE;
411-
412-
private static final TypeDescriptor OBJECT_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
413-
414-
/*
415-
* (non-Javadoc)
416-
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
417-
*/
418-
@Nullable
419-
@Override
420-
@SuppressWarnings("null")
421-
public Object convert(Object source) {
422-
423-
if (source instanceof io.vavr.collection.Traversable) {
424-
return VavrCollectionConverters.ToJavaConverter.INSTANCE //
425-
.convert(source, TypeDescriptor.forObject(source), OBJECT_DESCRIPTOR);
426-
}
427-
428-
return source;
429-
}
430-
431-
public WrapperType getWrapperType() {
432-
return WrapperType.multiValue(io.vavr.collection.Traversable.class);
433-
}
434-
}
435-
436404
private static class IterableToStreamableConverter implements ConditionalGenericConverter {
437405

438406
private static final TypeDescriptor STREAMABLE = TypeDescriptor.valueOf(Streamable.class);
@@ -446,6 +414,7 @@ private static class IterableToStreamableConverter implements ConditionalGeneric
446414
* (non-Javadoc)
447415
* @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
448416
*/
417+
@NonNull
449418
@Override
450419
public Set<ConvertiblePair> getConvertibleTypes() {
451420
return Collections.singleton(new ConvertiblePair(Iterable.class, Object.class));
@@ -514,7 +483,7 @@ public Cardinality getCardinality() {
514483
* @see java.lang.Object#equals(java.lang.Object)
515484
*/
516485
@Override
517-
public boolean equals(Object o) {
486+
public boolean equals(@Nullable Object o) {
518487

519488
if (this == o) {
520489
return true;
Lines changed: 90 additions & 0 deletions
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)