Skip to content

Commit a3afa84

Browse files
committed
DATACMNS-940 - Fix query method query result type detection for custom collections.
The nullable wrapper types now expose whether they're a single value type or a multi value type. This is necessary as Javaslang's Option implements Iterable (something we previously checked for to detect a collection query) and the custom collections potentially being usable within other wrapper types (e.g. Future<Seq<User>>).
1 parent 642e92b commit a3afa84

File tree

4 files changed

+138
-31
lines changed

4 files changed

+138
-31
lines changed

src/main/java/org/springframework/data/repository/query/QueryMethod.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,22 @@ public Class<?> getReturnedObjectType() {
173173
*/
174174
public boolean isCollectionQuery() {
175175

176-
return !(isPageQuery() || isSliceQuery())
177-
&& org.springframework.util.ClassUtils.isAssignable(Iterable.class, unwrappedReturnType)
176+
if (isPageQuery() || isSliceQuery()) {
177+
return false;
178+
}
179+
180+
Class<?> returnType = method.getReturnType();
181+
182+
if (QueryExecutionConverters.supports(returnType) && !QueryExecutionConverters.isSingleValue(returnType)) {
183+
return true;
184+
}
185+
186+
if (QueryExecutionConverters.supports(unwrappedReturnType)
187+
&& QueryExecutionConverters.isSingleValue(unwrappedReturnType)) {
188+
return false;
189+
}
190+
191+
return org.springframework.util.ClassUtils.isAssignable(Iterable.class, unwrappedReturnType)
178192
|| unwrappedReturnType.isArray();
179193
}
180194

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.core.convert.TypeDescriptor;
2929
import org.springframework.core.convert.converter.ConditionalGenericConverter;
3030
import org.springframework.core.convert.converter.Converter;
31+
import org.springframework.data.repository.util.QueryExecutionConverters.WrapperType;
3132
import org.springframework.util.ReflectionUtils;
3233

3334
/**
@@ -42,8 +43,8 @@ public enum ToJavaConverter implements Converter<Object, Object> {
4243

4344
INSTANCE;
4445

45-
public Class<?> getWrapperType() {
46-
return javaslang.collection.Traversable.class;
46+
public WrapperType getWrapperType() {
47+
return WrapperType.multiValue(javaslang.collection.Traversable.class);
4748
}
4849

4950
/*

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

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
package org.springframework.data.repository.util;
1717

1818
import javaslang.collection.Traversable;
19+
import lombok.AccessLevel;
20+
import lombok.Getter;
21+
import lombok.RequiredArgsConstructor;
22+
import lombok.Value;
1923
import scala.Function0;
2024
import scala.Option;
2125
import scala.runtime.AbstractFunction0;
@@ -28,6 +32,7 @@
2832
import java.util.concurrent.CompletableFuture;
2933
import java.util.concurrent.Future;
3034
import java.util.function.Supplier;
35+
import java.util.stream.Collectors;
3136

3237
import org.springframework.core.convert.ConversionService;
3338
import org.springframework.core.convert.TypeDescriptor;
@@ -72,16 +77,16 @@ public abstract class QueryExecutionConverters {
7277
private static final boolean JAVASLANG_PRESENT = ClassUtils.isPresent("javaslang.control.Option",
7378
QueryExecutionConverters.class.getClassLoader());
7479

75-
private static final Set<Class<?>> WRAPPER_TYPES = new HashSet<Class<?>>();
76-
private static final Set<Class<?>> UNWRAPPER_TYPES = new HashSet<Class<?>>();
80+
private static final Set<WrapperType> WRAPPER_TYPES = new HashSet<WrapperType>();
81+
private static final Set<WrapperType> UNWRAPPER_TYPES = new HashSet<WrapperType>();
7782
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<Converter<Object, Object>>();
7883

7984
static {
8085

81-
WRAPPER_TYPES.add(Future.class);
82-
UNWRAPPER_TYPES.add(Future.class);
83-
WRAPPER_TYPES.add(ListenableFuture.class);
84-
UNWRAPPER_TYPES.add(ListenableFuture.class);
86+
WRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
87+
UNWRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
88+
WRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
89+
UNWRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
8590

8691
if (GUAVA_PRESENT) {
8792
WRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType());
@@ -115,9 +120,12 @@ public abstract class QueryExecutionConverters {
115120
}
116121

117122
if (ReactiveWrappers.isAvailable()) {
118-
WRAPPER_TYPES.addAll(ReactiveWrappers.getNoValueTypes());
119-
WRAPPER_TYPES.addAll(ReactiveWrappers.getSingleValueTypes());
120-
WRAPPER_TYPES.addAll(ReactiveWrappers.getMultiValueTypes());
123+
WRAPPER_TYPES
124+
.addAll(ReactiveWrappers.getNoValueTypes().stream().map(WrapperType::noValue).collect(Collectors.toList()));
125+
WRAPPER_TYPES.addAll(
126+
ReactiveWrappers.getSingleValueTypes().stream().map(WrapperType::singleValue).collect(Collectors.toList()));
127+
WRAPPER_TYPES.addAll(
128+
ReactiveWrappers.getMultiValueTypes().stream().map(WrapperType::multiValue).collect(Collectors.toList()));
121129
}
122130
}
123131

@@ -133,8 +141,8 @@ public static boolean supports(Class<?> type) {
133141

134142
Assert.notNull(type, "Type must not be null!");
135143

136-
for (Class<?> candidate : WRAPPER_TYPES) {
137-
if (candidate.isAssignableFrom(type)) {
144+
for (WrapperType candidate : WRAPPER_TYPES) {
145+
if (candidate.getType().isAssignableFrom(type)) {
138146
return true;
139147
}
140148
}
@@ -152,15 +160,26 @@ public static boolean supportsUnwrapping(Class<?> type) {
152160

153161
Assert.notNull(type, "Type must not be null!");
154162

155-
for (Class<?> candidate : UNWRAPPER_TYPES) {
156-
if (candidate.isAssignableFrom(type)) {
163+
for (WrapperType candidate : UNWRAPPER_TYPES) {
164+
if (candidate.getType().isAssignableFrom(type)) {
157165
return true;
158166
}
159167
}
160168

161169
return false;
162170
}
163171

172+
public static boolean isSingleValue(Class<?> type) {
173+
174+
for (WrapperType candidate : WRAPPER_TYPES) {
175+
if (candidate.getType().isAssignableFrom(type)) {
176+
return candidate.isSingleValue();
177+
}
178+
}
179+
180+
return false;
181+
}
182+
164183
/**
165184
* Registers converters for wrapper types found on the classpath.
166185
*
@@ -311,8 +330,8 @@ protected Object wrap(Object source) {
311330
return Optional.of(source);
312331
}
313332

314-
public static Class<?> getWrapperType() {
315-
return Optional.class;
333+
public static WrapperType getWrapperType() {
334+
return WrapperType.singleValue(Optional.class);
316335
}
317336
}
318337

@@ -341,8 +360,8 @@ protected Object wrap(Object source) {
341360
return java.util.Optional.of(source);
342361
}
343362

344-
public static Class<?> getWrapperType() {
345-
return java.util.Optional.class;
363+
public static WrapperType getWrapperType() {
364+
return WrapperType.singleValue(java.util.Optional.class);
346365
}
347366
}
348367

@@ -397,8 +416,8 @@ protected Object wrap(Object source) {
397416
return source instanceof CompletableFuture ? source : CompletableFuture.completedFuture(source);
398417
}
399418

400-
public static Class<?> getWrapperType() {
401-
return CompletableFuture.class;
419+
public static WrapperType getWrapperType() {
420+
return WrapperType.singleValue(CompletableFuture.class);
402421
}
403422
}
404423

@@ -423,8 +442,8 @@ protected Object wrap(Object source) {
423442
return Option.apply(source);
424443
}
425444

426-
public static Class<?> getWrapperType() {
427-
return Option.class;
445+
public static WrapperType getWrapperType() {
446+
return WrapperType.singleValue(Option.class);
428447
}
429448
}
430449

@@ -440,8 +459,8 @@ private static class NullableWrapperToJavaslangOptionConverter extends AbstractW
440459
private static final Method NONE_METHOD;
441460

442461
static {
443-
OF_METHOD = ReflectionUtils.findMethod(getWrapperType(), "of", Object.class);
444-
NONE_METHOD = ReflectionUtils.findMethod(getWrapperType(), "none");
462+
OF_METHOD = ReflectionUtils.findMethod(javaslang.control.Option.class, "of", Object.class);
463+
NONE_METHOD = ReflectionUtils.findMethod(javaslang.control.Option.class, "none");
445464
}
446465

447466
/**
@@ -450,11 +469,11 @@ private static class NullableWrapperToJavaslangOptionConverter extends AbstractW
450469
* @param conversionService must not be {@literal null}.
451470
*/
452471
public NullableWrapperToJavaslangOptionConverter(ConversionService conversionService) {
453-
super(conversionService, createEmptyOption(), getWrapperType());
472+
super(conversionService, createEmptyOption(), javaslang.control.Option.class);
454473
}
455474

456-
public static Class<?> getWrapperType() {
457-
return javaslang.control.Option.class;
475+
public static WrapperType getWrapperType() {
476+
return WrapperType.singleValue(javaslang.control.Option.class);
458477
}
459478

460479
/*
@@ -586,4 +605,32 @@ public Object convert(Object source) {
586605
return source;
587606
}
588607
}
608+
609+
@Value
610+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
611+
public static class WrapperType {
612+
613+
enum Cardinality {
614+
NONE, SINGLE, MULTI;
615+
}
616+
617+
Class<?> type;
618+
@Getter(AccessLevel.NONE) Cardinality cardinality;
619+
620+
public static WrapperType singleValue(Class<?> type) {
621+
return new WrapperType(type, Cardinality.SINGLE);
622+
}
623+
624+
public static WrapperType multiValue(Class<?> type) {
625+
return new WrapperType(type, Cardinality.MULTI);
626+
}
627+
628+
public static WrapperType noValue(Class<?> type) {
629+
return new WrapperType(type, Cardinality.NONE);
630+
}
631+
632+
boolean isSingleValue() {
633+
return cardinality.equals(Cardinality.SINGLE);
634+
}
635+
}
589636
}

src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2008-2015 the original author or authors.
2+
* Copyright 2008-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,9 @@
1919
import static org.junit.Assert.*;
2020
import static org.junit.Assume.*;
2121

22+
import javaslang.collection.Seq;
23+
import javaslang.control.Option;
24+
2225
import java.io.Serializable;
2326
import java.lang.reflect.Method;
2427
import java.util.List;
@@ -181,6 +184,42 @@ public void doesNotRejectFutureQueryForEntityCollection() throws Exception {
181184
assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true));
182185
}
183186

187+
/**
188+
* @see DATACMNS-940
189+
*/
190+
@Test
191+
public void detectsCustomCollectionReturnType() throws Exception {
192+
193+
RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class);
194+
Method method = SampleRepository.class.getMethod("returnsSeq");
195+
196+
assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true));
197+
}
198+
199+
/**
200+
* @see DATACMNS-940
201+
*/
202+
@Test
203+
public void detectsWrapperWithinWrapper() throws Exception {
204+
205+
RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class);
206+
Method method = SampleRepository.class.getMethod("returnsFutureOfSeq");
207+
208+
assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true));
209+
}
210+
211+
/**
212+
* @see DATACMNS-940
213+
*/
214+
@Test
215+
public void detectsSinglValueWrapperWithinWrapper() throws Exception {
216+
217+
RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class);
218+
Method method = SampleRepository.class.getMethod("returnsFutureOfOption");
219+
220+
assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(false));
221+
}
222+
184223
interface SampleRepository extends Repository<User, Serializable> {
185224

186225
String pagingMethodWithInvalidReturnType(Pageable pageable);
@@ -216,6 +255,12 @@ interface SampleRepository extends Repository<User, Serializable> {
216255

217256
// DATACMNS-716
218257
Future<List<User>> returnsFutureForEntityCollection();
258+
259+
Seq<User> returnsSeq();
260+
261+
Future<Seq<User>> returnsFutureOfSeq();
262+
263+
Future<Option<User>> returnsFutureOfOption();
219264
}
220265

221266
class User {

0 commit comments

Comments
 (0)