diff --git a/src/main/asciidoc/repository-projections.adoc b/src/main/asciidoc/repository-projections.adoc index 7911d9b92..9b70c408d 100644 --- a/src/main/asciidoc/repository-projections.adoc +++ b/src/main/asciidoc/repository-projections.adoc @@ -197,6 +197,30 @@ interface NamesOnly { Again, for more complex expressions, you should use a Spring bean and let the expression invoke a method, as described <>. +[[projections.interfaces.nullable-wrappers]] +=== Nullable Wrappers + +Getters in projection interfaces can make use of nullable wrappers for improved null-safety. Currently supported wrapper types are: + +* `java.util.Optional` +* `com.google.common.base.Optional` +* `scala.Option` +* `io.vavr.control.Option` + +.A projection interface using nullable wrappers +==== +[source, java] +---- +interface NamesOnly { + + Optional getFirstname(); +} +---- +==== + +If the underlying projection value is not `null`, then values are returned using the present-representation of the wrapper type. +In case the backing value is `null`, then the getter method returns the empty representation of the used wrapper type. + [[projections.dtos]] == Class-based Projections (DTOs) diff --git a/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java index 655087969..063b3d64e 100644 --- a/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java @@ -31,6 +31,8 @@ import org.aopalliance.intercept.MethodInvocation; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.NullableWrapper; +import org.springframework.data.util.NullableWrapperConverters; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -67,23 +69,43 @@ class ProjectingMethodInterceptor implements MethodInterceptor { @Override public Object invoke(@SuppressWarnings("null") @Nonnull MethodInvocation invocation) throws Throwable { + TypeInformation type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod()); + TypeInformation resultType = type; + TypeInformation typeToReturn = type; + Object result = delegate.invoke(invocation); + boolean applyWrapper = false; + + if (NullableWrapperConverters.supports(type.getType()) + && (result == null || !NullableWrapperConverters.supports(result.getClass()))) { + resultType = NullableWrapperConverters.unwrapActualType(typeToReturn); + applyWrapper = true; + } + + result = potentiallyConvertResult(resultType, result); + + if (applyWrapper) { + return conversionService.convert(new NullableWrapper(result), typeToReturn.getType()); + } + + return result; + } + + @Nullable + protected Object potentiallyConvertResult(TypeInformation type, @Nullable Object result) { if (result == null) { return null; } - TypeInformation type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod()); - Class rawType = type.getType(); - - if (type.isCollectionLike() && !ClassUtils.isPrimitiveArray(rawType)) { + if (type.isCollectionLike() && !ClassUtils.isPrimitiveArray(type.getType())) { return projectCollectionElements(asCollection(result), type); } else if (type.isMap()) { return projectMapValues((Map) result, type); - } else if (conversionRequiredAndPossible(result, rawType)) { - return conversionService.convert(result, rawType); + } else if (conversionRequiredAndPossible(result, type.getType())) { + return conversionService.convert(result, type.getType()); } else { - return getProjection(result, rawType); + return getProjection(result, type.getType()); } } diff --git a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java index b267cc78f..ed3157011 100644 --- a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java +++ b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java @@ -26,8 +26,9 @@ import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.util.NullableWrapperConverters; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -47,8 +48,14 @@ import org.springframework.util.ConcurrentReferenceHashMap; */ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware { + final static GenericConversionService CONVERSION_SERVICE = new DefaultConversionService(); + + static { + NullableWrapperConverters.registerConvertersIn(CONVERSION_SERVICE); + CONVERSION_SERVICE.removeConvertible(Object.class, Object.class); + } + private final List factories; - private final ConversionService conversionService; private final Map, ProjectionInformation> projectionInformationCache = new ConcurrentReferenceHashMap<>(); private @Nullable ClassLoader classLoader; @@ -60,8 +67,6 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware this.factories = new ArrayList<>(); this.factories.add(MapAccessingMethodInterceptorFactory.INSTANCE); this.factories.add(PropertyAccessingMethodInvokerFactory.INSTANCE); - - this.conversionService = DefaultConversionService.getSharedInstance(); } /* @@ -174,7 +179,7 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware .createMethodInterceptor(source, projectionType); return new ProjectingMethodInterceptor(this, - postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType), conversionService); + postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType), CONVERSION_SERVICE); } /** diff --git a/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java index e486db511..2a887171a 100755 --- a/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java @@ -74,12 +74,11 @@ class ProjectingMethodInterceptorUnitTests { assertThat(methodInterceptor.invoke(invocation)).isEqualTo("Foo"); } - @Test // DATAREST-221 + @Test // DATAREST-221, DATACMNS-1762 void returnsNullAsIs() throws Throwable { MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor, conversionService); - - when(interceptor.invoke(invocation)).thenReturn(null); + mockInvocationOf("getString", null); assertThat(methodInterceptor.invoke(invocation)).isNull(); }