Skip to content

Commit

Permalink
DATACMNS-483 - Support for wrapper types as return types for reposito…
Browse files Browse the repository at this point in the history
…ry 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
  • Loading branch information
odrotbohm committed Apr 22, 2014
1 parent 165c059 commit c141fb7
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 22 deletions.
42 changes: 29 additions & 13 deletions pom.xml
Expand Up @@ -2,7 +2,7 @@
<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">

<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>1.8.0.BUILD-SNAPSHOT</version>
Expand All @@ -15,13 +15,14 @@
<version>1.4.0.BUILD-SNAPSHOT</version>
<relativePath>../spring-data-build/parent/pom.xml</relativePath>
</parent>

<properties>
<guava>16.0.1</guava>
<jackson1>1.9.7</jackson1>
<springhateoas>0.10.0.RELEASE</springhateoas>
<dist.key>DATACMNS</dist.key>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
Expand Down Expand Up @@ -74,7 +75,7 @@
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
Expand Down Expand Up @@ -109,7 +110,7 @@
<version>${querydsl}</version>
<scope>provided</scope>
</dependency>

<!-- EJB Transactions -->
<dependency>
<groupId>javax.ejb</groupId>
Expand All @@ -126,21 +127,35 @@
<scope>provided</scope>
<optional>true</optional>
</dependency>


<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-java8-stub</artifactId>
<version>1.4.0.BUILD-SNAPSHOT</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
<version>${cdi}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.openwebbeans.test</groupId>
<artifactId>cditest-owb</artifactId>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
Expand All @@ -152,14 +167,14 @@
<artifactId>spring-webmvc</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.3U1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>xmlunit</groupId>
<artifactId>xmlunit</artifactId>
Expand All @@ -175,9 +190,10 @@
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>

<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
Expand Down Expand Up @@ -205,14 +221,14 @@
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-libs-snapshot</id>
<url>http://repo.spring.io/libs-snapshot</url>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>spring-plugins-release</id>
Expand Down
Expand Up @@ -21,6 +21,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -60,7 +61,8 @@ public Class<?> getReturnedDomainClass(Method method) {
TypeInformation<?> returnTypeInfo = typeInformation.getReturnType(method);
Class<?> rawType = returnTypeInfo.getType();

boolean needToUnwrap = Iterable.class.isAssignableFrom(rawType) || rawType.isArray();
boolean needToUnwrap = Iterable.class.isAssignableFrom(rawType) || rawType.isArray()
|| QueryExecutionConverters.supports(rawType);

return needToUnwrap ? returnTypeInfo.getComponentType().getType() : rawType;
}
Expand Down
Expand Up @@ -30,6 +30,9 @@
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
Expand All @@ -40,6 +43,8 @@
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.NullableWrapper;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

Expand All @@ -52,6 +57,8 @@
*/
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {

private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class);

private final Map<RepositoryInformationCacheKey, RepositoryInformation> REPOSITORY_INFORMATION_CACHE = new HashMap<RepositoryInformationCacheKey, RepositoryInformation>();

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

private final Object customImplementation;
private final RepositoryInformation repositoryInformation;
private final GenericConversionService conversionService;
private final Object target;

/**
Expand All @@ -282,6 +290,13 @@ public class QueryExecutorMethodInterceptor implements MethodInterceptor {
public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, Object customImplementation,
Object target) {

Assert.notNull(repositoryInformation, "RepositoryInformation must not be null!");
Assert.notNull(target, "Target must not be null!");

DefaultConversionService conversionService = new DefaultConversionService();
QueryExecutionConverters.registerConvertersIn(conversionService);
this.conversionService = conversionService;

this.repositoryInformation = repositoryInformation;
this.customImplementation = customImplementation;
this.target = target;
Expand Down Expand Up @@ -325,22 +340,45 @@ private void invokeListeners(RepositoryQuery query) {
*/
public Object invoke(MethodInvocation invocation) throws Throwable {

Object result = doInvoke(invocation);
Class<?> expectedReturnType = invocation.getMethod().getReturnType();

if (result != null && expectedReturnType.isInstance(result)) {
return result;
}

if (conversionService.canConvert(NullableWrapper.class, expectedReturnType)
&& !conversionService.canBypassConvert(WRAPPER_TYPE, TypeDescriptor.valueOf(expectedReturnType))) {
return conversionService.convert(new NullableWrapper(result), expectedReturnType);
}

if (result == null) {
return null;
}

return conversionService.canConvert(result.getClass(), expectedReturnType) ? conversionService.convert(result,
expectedReturnType) : result;
}

private Object doInvoke(MethodInvocation invocation) throws Throwable {

Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();

if (isCustomMethodInvocation(invocation)) {
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
makeAccessible(actualMethod);
return executeMethodOn(customImplementation, actualMethod, invocation.getArguments());
return executeMethodOn(customImplementation, actualMethod, arguments);
}

if (hasQueryFor(method)) {
return queries.get(method).execute(invocation.getArguments());
return queries.get(method).execute(arguments);
}

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

/**
Expand Down Expand Up @@ -370,7 +408,6 @@ private Object executeMethodOn(Object target, Method method, Object[] parameters
* @return
*/
private boolean hasQueryFor(Method method) {

return queries.containsKey(method);
}

Expand Down
Expand Up @@ -40,6 +40,8 @@ public class QueryMethod {
private final Method method;
private final Parameters<?, ?> parameters;

private Class<?> domainClass;

/**
* Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following
* invocations of the method given.
Expand Down Expand Up @@ -129,11 +131,16 @@ public String getNamedQueryName() {
*/
protected Class<?> getDomainClass() {

Class<?> repositoryDomainClass = metadata.getDomainType();
Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);
if (domainClass == null) {

Class<?> repositoryDomainClass = metadata.getDomainType();
Class<?> methodDomainClass = metadata.getReturnedDomainClass(method);

this.domainClass = repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass) ? methodDomainClass
: repositoryDomainClass;
}

return repositoryDomainClass == null || repositoryDomainClass.isAssignableFrom(methodDomainClass) ? methodDomainClass
: repositoryDomainClass;
return domainClass;
}

/**
Expand Down
@@ -0,0 +1,58 @@
/*
* Copyright 2014 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
*
* http://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.repository.util;

import org.springframework.core.convert.converter.Converter;

/**
* Simple value object to wrap a nullable delegate. Used to be able to write {@link Converter} implementations that
* convert {@literal null} into an object of some sort.
*
* @author Oliver Gierke
* @since 1.8
* @see QueryExecutionConverters
*/
public class NullableWrapper {

private final Object value;

/**
* Creates a new {@link NullableWrapper} for the given value.
*
* @param value can be {@literal null}.
*/
public NullableWrapper(Object value) {
this.value = value;
}

/**
* Returns the type of the contained value. WIll fall back to {@link Object} in case the value is {@literal null}.
*
* @return will never be {@literal null}.
*/
public Class<?> getValueType() {
return value == null ? Object.class : value.getClass();
}

/**
* Returns the backing valie
*
* @return the value can be {@literal null}.
*/
public Object getValue() {
return value;
}
}

0 comments on commit c141fb7

Please sign in to comment.