diff --git a/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java b/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java new file mode 100644 index 000000000..b1c65a29a --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java @@ -0,0 +1,78 @@ +/* + * 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.core.support; + +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.util.NullableWrapper; +import org.springframework.data.repository.util.QueryExecutionConverters; + +/** + * Simple domain service to convert query results into a dedicated type. + * + * @author Oliver Gierke + */ +class QueryExecutionResultHandler { + + private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class); + + private final GenericConversionService conversionService; + + /** + * Creates a new {@link QueryExecutionResultHandler}. + */ + public QueryExecutionResultHandler() { + + GenericConversionService conversionService = new DefaultConversionService(); + QueryExecutionConverters.registerConvertersIn(conversionService); + + this.conversionService = conversionService; + } + + /** + * Post-processes the given result of a query invocation to the given type. + * + * @param result can be {@literal null}. + * @param returnTypeDesciptor can be {@literal null}, if so, no conversion is performed. + * @return + */ + public Object postProcessInvocationResult(Object result, TypeDescriptor returnTypeDesciptor) { + + if (returnTypeDesciptor == null) { + return result; + } + + Class expectedReturnType = returnTypeDesciptor.getType(); + + if (result != null && expectedReturnType.isInstance(result)) { + return result; + } + + if (QueryExecutionConverters.supports(expectedReturnType) + && conversionService.canConvert(WRAPPER_TYPE, returnTypeDesciptor) + && !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; + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 29299f885..42dce390c 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -35,8 +35,6 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; 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; @@ -49,8 +47,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; 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; @@ -63,7 +59,6 @@ import org.springframework.util.ObjectUtils; */ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { - private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class); private static final boolean IS_JAVA_8 = org.springframework.util.ClassUtils.isPresent("java.util.Optional", RepositoryFactorySupport.class.getClassLoader()); @@ -316,7 +311,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { private final Object customImplementation; private final RepositoryInformation repositoryInformation; - private final GenericConversionService conversionService; + private final QueryExecutionResultHandler resultHandler; private final Object target; /** @@ -329,10 +324,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { 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.resultHandler = new QueryExecutionResultHandler(); this.repositoryInformation = repositoryInformation; this.customImplementation = customImplementation; this.target = target; @@ -380,30 +372,12 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { Object result = doInvoke(invocation); - Method method = invocation.getMethod(); - // Looking up the TypeDescriptor for the return type - yes, this way o.O + Method method = invocation.getMethod(); MethodParameter parameter = new MethodParameter(method, -1); TypeDescriptor methodReturnTypeDescriptor = TypeDescriptor.nested(parameter, 0); - Class expectedReturnType = method.getReturnType(); - - if (result != null && expectedReturnType.isInstance(result)) { - return result; - } - - if (QueryExecutionConverters.supports(expectedReturnType) - && conversionService.canConvert(WRAPPER_TYPE, methodReturnTypeDescriptor) - && !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; + return resultHandler.postProcessInvocationResult(result, methodReturnTypeDescriptor); } private Object doInvoke(MethodInvocation invocation) throws Throwable { diff --git a/src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java new file mode 100644 index 000000000..773f79377 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java @@ -0,0 +1,123 @@ +/* + * 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.core.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.repository.Repository; + +/** + * Unit tests for {@link QueryExecutionResultHandler}. + * + * @author Oliver Gierke + */ +public class QueryExecutionResultHandlerUnitTests { + + QueryExecutionResultHandler handler = new QueryExecutionResultHandler(); + + /** + * @see DATACMNS-610 + */ + @Test + public void convertsListsToSet() throws Exception { + + TypeDescriptor descriptor = getTypeDescriptorFor("set"); + List source = Collections.singletonList(new Entity()); + + assertThat(handler.postProcessInvocationResult(source, descriptor), is(instanceOf(Set.class))); + } + + /** + * @see DATACMNS-483 + */ + @Test + public void turnsNullIntoJdk8Optional() throws Exception { + + Object result = handler.postProcessInvocationResult(null, getTypeDescriptorFor("jdk8Optional")); + assertThat(result, is((Object) Optional.empty())); + } + + /** + * @see DATACMNS-483 + */ + @Test + @SuppressWarnings("unchecked") + public void wrapsValueIntoJdk8Optional() throws Exception { + + Entity entity = new Entity(); + + Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("jdk8Optional")); + assertThat(result, is(instanceOf(Optional.class))); + + Optional optional = (Optional) result; + assertThat(optional, is(Optional.of(entity))); + } + + /** + * @see DATACMNS-483 + */ + @Test + public void turnsNullIntoGuavaOptional() throws Exception { + + Object result = handler.postProcessInvocationResult(null, getTypeDescriptorFor("guavaOptional")); + assertThat(result, is((Object) com.google.common.base.Optional.absent())); + } + + /** + * @see DATACMNS-483 + */ + @Test + @SuppressWarnings("unchecked") + public void wrapsValueIntoGuavaOptional() throws Exception { + + Entity entity = new Entity(); + + Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("guavaOptional")); + assertThat(result, is(instanceOf(com.google.common.base.Optional.class))); + + com.google.common.base.Optional optional = (com.google.common.base.Optional) result; + assertThat(optional, is(com.google.common.base.Optional.of(entity))); + } + + private static TypeDescriptor getTypeDescriptorFor(String methodName) throws Exception { + + Method method = Sample.class.getMethod(methodName); + MethodParameter parameter = new MethodParameter(method, -1); + + return TypeDescriptor.nested(parameter, 0); + } + + static interface Sample extends Repository { + + Set set(); + + Optional jdk8Optional(); + + com.google.common.base.Optional guavaOptional(); + } + + static class Entity {} +}