Skip to content

Commit c141fb7

Browse files
committed
DATACMNS-483 - Support for wrapper types as return types for repository methods.
The repository method invocation mechanism now post-processes the result of the actual method invocation in case it's not type-compatible with the return type of the invoked method and invokes a conversion if applicable. This allows us to register custom converters for well-known wrapper types. We currently support Optional from both Google Guava and JDK 8. Implementation notice We use some JDK 8 type stubs from the Spring Data Build project to be able to compile using JDKs < 8. This is necessary as we need to remain compatible with Spring 3.2.x for the Dijkstra release train and our test executions still run into some issues with it a Java 8 runtime (mostly ASM related). These issues were reported in [0, 1] and we'll probably get off the hack for Dijkstra GA by upgrading to Spring 3.2.9. [0] https://jira.spring.io/browse/SPR-11719 [1] https://jira.spring.io/browse/SPR-11718
1 parent 165c059 commit c141fb7

File tree

9 files changed

+368
-22
lines changed

9 files changed

+368
-22
lines changed

pom.xml

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
33

44
<modelVersion>4.0.0</modelVersion>
5-
5+
66
<groupId>org.springframework.data</groupId>
77
<artifactId>spring-data-commons</artifactId>
88
<version>1.8.0.BUILD-SNAPSHOT</version>
@@ -15,13 +15,14 @@
1515
<version>1.4.0.BUILD-SNAPSHOT</version>
1616
<relativePath>../spring-data-build/parent/pom.xml</relativePath>
1717
</parent>
18-
18+
1919
<properties>
20+
<guava>16.0.1</guava>
2021
<jackson1>1.9.7</jackson1>
2122
<springhateoas>0.10.0.RELEASE</springhateoas>
2223
<dist.key>DATACMNS</dist.key>
2324
</properties>
24-
25+
2526
<dependencies>
2627
<dependency>
2728
<groupId>org.springframework</groupId>
@@ -74,7 +75,7 @@
7475
<artifactId>spring-web</artifactId>
7576
<optional>true</optional>
7677
</dependency>
77-
78+
7879
<dependency>
7980
<groupId>javax.servlet</groupId>
8081
<artifactId>servlet-api</artifactId>
@@ -109,7 +110,7 @@
109110
<version>${querydsl}</version>
110111
<scope>provided</scope>
111112
</dependency>
112-
113+
113114
<!-- EJB Transactions -->
114115
<dependency>
115116
<groupId>javax.ejb</groupId>
@@ -126,21 +127,35 @@
126127
<scope>provided</scope>
127128
<optional>true</optional>
128129
</dependency>
129-
130+
131+
<dependency>
132+
<groupId>com.google.guava</groupId>
133+
<artifactId>guava</artifactId>
134+
<version>${guava}</version>
135+
<optional>true</optional>
136+
</dependency>
137+
138+
<dependency>
139+
<groupId>org.springframework.data.build</groupId>
140+
<artifactId>spring-data-java8-stub</artifactId>
141+
<version>1.4.0.BUILD-SNAPSHOT</version>
142+
<scope>provided</scope>
143+
</dependency>
144+
130145
<dependency>
131146
<groupId>javax.el</groupId>
132147
<artifactId>el-api</artifactId>
133148
<version>${cdi}</version>
134149
<scope>test</scope>
135150
</dependency>
136-
151+
137152
<dependency>
138153
<groupId>org.apache.openwebbeans.test</groupId>
139154
<artifactId>cditest-owb</artifactId>
140155
<version>${webbeans}</version>
141156
<scope>test</scope>
142157
</dependency>
143-
158+
144159
<dependency>
145160
<groupId>org.springframework.hateoas</groupId>
146161
<artifactId>spring-hateoas</artifactId>
@@ -152,14 +167,14 @@
152167
<artifactId>spring-webmvc</artifactId>
153168
<optional>true</optional>
154169
</dependency>
155-
170+
156171
<dependency>
157172
<groupId>com.sun.xml.bind</groupId>
158173
<artifactId>jaxb-impl</artifactId>
159174
<version>2.2.3U1</version>
160175
<scope>test</scope>
161176
</dependency>
162-
177+
163178
<dependency>
164179
<groupId>xmlunit</groupId>
165180
<artifactId>xmlunit</artifactId>
@@ -175,9 +190,10 @@
175190
<scope>test</scope>
176191
</dependency>
177192
</dependencies>
178-
193+
179194
<build>
180195
<plugins>
196+
181197
<plugin>
182198
<groupId>com.mysema.maven</groupId>
183199
<artifactId>apt-maven-plugin</artifactId>
@@ -205,14 +221,14 @@
205221
</plugin>
206222
</plugins>
207223
</build>
208-
224+
209225
<repositories>
210226
<repository>
211227
<id>spring-libs-snapshot</id>
212228
<url>http://repo.spring.io/libs-snapshot</url>
213229
</repository>
214230
</repositories>
215-
231+
216232
<pluginRepositories>
217233
<pluginRepository>
218234
<id>spring-plugins-release</id>

src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.data.domain.Pageable;
2222
import org.springframework.data.repository.core.CrudMethods;
2323
import org.springframework.data.repository.core.RepositoryMetadata;
24+
import org.springframework.data.repository.util.QueryExecutionConverters;
2425
import org.springframework.data.util.ClassTypeInformation;
2526
import org.springframework.data.util.TypeInformation;
2627
import org.springframework.util.Assert;
@@ -60,7 +61,8 @@ public Class<?> getReturnedDomainClass(Method method) {
6061
TypeInformation<?> returnTypeInfo = typeInformation.getReturnType(method);
6162
Class<?> rawType = returnTypeInfo.getType();
6263

63-
boolean needToUnwrap = Iterable.class.isAssignableFrom(rawType) || rawType.isArray();
64+
boolean needToUnwrap = Iterable.class.isAssignableFrom(rawType) || rawType.isArray()
65+
|| QueryExecutionConverters.supports(rawType);
6466

6567
return needToUnwrap ? returnTypeInfo.getComponentType().getType() : rawType;
6668
}

src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
import org.springframework.aop.framework.ProxyFactory;
3131
import org.springframework.beans.factory.BeanClassLoaderAware;
3232
import org.springframework.core.GenericTypeResolver;
33+
import org.springframework.core.convert.TypeDescriptor;
34+
import org.springframework.core.convert.support.DefaultConversionService;
35+
import org.springframework.core.convert.support.GenericConversionService;
3336
import org.springframework.data.repository.Repository;
3437
import org.springframework.data.repository.core.EntityInformation;
3538
import org.springframework.data.repository.core.NamedQueries;
@@ -40,6 +43,8 @@
4043
import org.springframework.data.repository.query.QueryMethod;
4144
import org.springframework.data.repository.query.RepositoryQuery;
4245
import org.springframework.data.repository.util.ClassUtils;
46+
import org.springframework.data.repository.util.NullableWrapper;
47+
import org.springframework.data.repository.util.QueryExecutionConverters;
4348
import org.springframework.util.Assert;
4449
import org.springframework.util.ObjectUtils;
4550

@@ -52,6 +57,8 @@
5257
*/
5358
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
5459

60+
private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class);
61+
5562
private final Map<RepositoryInformationCacheKey, RepositoryInformation> REPOSITORY_INFORMATION_CACHE = new HashMap<RepositoryInformationCacheKey, RepositoryInformation>();
5663

5764
private final List<RepositoryProxyPostProcessor> postProcessors = new ArrayList<RepositoryProxyPostProcessor>();
@@ -273,6 +280,7 @@ public class QueryExecutorMethodInterceptor implements MethodInterceptor {
273280

274281
private final Object customImplementation;
275282
private final RepositoryInformation repositoryInformation;
283+
private final GenericConversionService conversionService;
276284
private final Object target;
277285

278286
/**
@@ -282,6 +290,13 @@ public class QueryExecutorMethodInterceptor implements MethodInterceptor {
282290
public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, Object customImplementation,
283291
Object target) {
284292

293+
Assert.notNull(repositoryInformation, "RepositoryInformation must not be null!");
294+
Assert.notNull(target, "Target must not be null!");
295+
296+
DefaultConversionService conversionService = new DefaultConversionService();
297+
QueryExecutionConverters.registerConvertersIn(conversionService);
298+
this.conversionService = conversionService;
299+
285300
this.repositoryInformation = repositoryInformation;
286301
this.customImplementation = customImplementation;
287302
this.target = target;
@@ -325,22 +340,45 @@ private void invokeListeners(RepositoryQuery query) {
325340
*/
326341
public Object invoke(MethodInvocation invocation) throws Throwable {
327342

343+
Object result = doInvoke(invocation);
344+
Class<?> expectedReturnType = invocation.getMethod().getReturnType();
345+
346+
if (result != null && expectedReturnType.isInstance(result)) {
347+
return result;
348+
}
349+
350+
if (conversionService.canConvert(NullableWrapper.class, expectedReturnType)
351+
&& !conversionService.canBypassConvert(WRAPPER_TYPE, TypeDescriptor.valueOf(expectedReturnType))) {
352+
return conversionService.convert(new NullableWrapper(result), expectedReturnType);
353+
}
354+
355+
if (result == null) {
356+
return null;
357+
}
358+
359+
return conversionService.canConvert(result.getClass(), expectedReturnType) ? conversionService.convert(result,
360+
expectedReturnType) : result;
361+
}
362+
363+
private Object doInvoke(MethodInvocation invocation) throws Throwable {
364+
328365
Method method = invocation.getMethod();
366+
Object[] arguments = invocation.getArguments();
329367

330368
if (isCustomMethodInvocation(invocation)) {
331369
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
332370
makeAccessible(actualMethod);
333-
return executeMethodOn(customImplementation, actualMethod, invocation.getArguments());
371+
return executeMethodOn(customImplementation, actualMethod, arguments);
334372
}
335373

336374
if (hasQueryFor(method)) {
337-
return queries.get(method).execute(invocation.getArguments());
375+
return queries.get(method).execute(arguments);
338376
}
339377

340378
// Lookup actual method as it might be redeclared in the interface
341379
// and we have to use the repository instance nevertheless
342380
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
343-
return executeMethodOn(target, actualMethod, invocation.getArguments());
381+
return executeMethodOn(target, actualMethod, arguments);
344382
}
345383

346384
/**
@@ -370,7 +408,6 @@ private Object executeMethodOn(Object target, Method method, Object[] parameters
370408
* @return
371409
*/
372410
private boolean hasQueryFor(Method method) {
373-
374411
return queries.containsKey(method);
375412
}
376413

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public class QueryMethod {
4040
private final Method method;
4141
private final Parameters<?, ?> parameters;
4242

43+
private Class<?> domainClass;
44+
4345
/**
4446
* Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following
4547
* invocations of the method given.
@@ -129,11 +131,16 @@ public String getNamedQueryName() {
129131
*/
130132
protected Class<?> getDomainClass() {
131133

132-
Class<?> repositoryDomainClass = metadata.getDomainType();
133-
Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);
134+
if (domainClass == null) {
135+
136+
Class<?> repositoryDomainClass = metadata.getDomainType();
137+
Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);
138+
139+
this.domainClass = repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass) ? methodDomainClass
140+
: repositoryDomainClass;
141+
}
134142

135-
return repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass) ? methodDomainClass
136-
: repositoryDomainClass;
143+
return domainClass;
137144
}
138145

139146
/**
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2014 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+
* http://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.repository.util;
17+
18+
import org.springframework.core.convert.converter.Converter;
19+
20+
/**
21+
* Simple value object to wrap a nullable delegate. Used to be able to write {@link Converter} implementations that
22+
* convert {@literal null} into an object of some sort.
23+
*
24+
* @author Oliver Gierke
25+
* @since 1.8
26+
* @see QueryExecutionConverters
27+
*/
28+
public class NullableWrapper {
29+
30+
private final Object value;
31+
32+
/**
33+
* Creates a new {@link NullableWrapper} for the given value.
34+
*
35+
* @param value can be {@literal null}.
36+
*/
37+
public NullableWrapper(Object value) {
38+
this.value = value;
39+
}
40+
41+
/**
42+
* Returns the type of the contained value. WIll fall back to {@link Object} in case the value is {@literal null}.
43+
*
44+
* @return will never be {@literal null}.
45+
*/
46+
public Class<?> getValueType() {
47+
return value == null ? Object.class : value.getClass();
48+
}
49+
50+
/**
51+
* Returns the backing valie
52+
*
53+
* @return the value can be {@literal null}.
54+
*/
55+
public Object getValue() {
56+
return value;
57+
}
58+
}

0 commit comments

Comments
 (0)