From d8781888d8fda950d7fbbaddcdbc791e7f63e5c4 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 4 Dec 2014 20:25:11 +0100 Subject: [PATCH] DATACMNS-610 - Extracted QueryExecutionResultHandler from RepositoryFactorySupport. To be able to verify the conversion of List based query execution results into Sets I extracted a QueryExecutionResultHandler that applies the already implemented handling for Optional types and eventually general prost processing in case the return type declared at the repository query method doesn't match the returned value. Added additional unit tests for Optional handling. --- .../support/QueryExecutionResultHandler.java | 78 +++++++++++ .../support/RepositoryFactorySupport.java | 34 +---- .../QueryExecutionResultHandlerUnitTests.java | 123 ++++++++++++++++++ 3 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/core/support/QueryExecutionResultHandler.java create mode 100644 src/test/java/org/springframework/data/repository/core/support/QueryExecutionResultHandlerUnitTests.java 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 {} +}