Browse Source

DATACMNS-1762 - Support Optional wrapping for projection getters.

We now support nullable wrappers for projection interfaces. Getters are inspected whether their return type is a supported nullable wrapper. If so, then the value can be wrapped into that type. Null values default in that case to their corresponding empty wrapper representation.

Original Pull Request: #459
pull/469/head
Mark Paluch 6 years ago committed by Christoph Strobl
parent
commit
bd3992dfc5
  1. 24
      src/main/asciidoc/repository-projections.adoc
  2. 36
      src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java
  3. 15
      src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java
  4. 5
      src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java

24
src/main/asciidoc/repository-projections.adoc

@ -197,6 +197,30 @@ interface NamesOnly { @@ -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.open.bean-reference,earlier>>.
[[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<String> 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)

36
src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java

@ -31,6 +31,8 @@ import org.aopalliance.intercept.MethodInvocation; @@ -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 { @@ -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());
}
}

15
src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java

@ -26,8 +26,9 @@ import org.aopalliance.intercept.MethodInvocation; @@ -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; @@ -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<MethodInterceptorFactory> factories;
private final ConversionService conversionService;
private final Map<Class<?>, ProjectionInformation> projectionInformationCache = new ConcurrentReferenceHashMap<>();
private @Nullable ClassLoader classLoader;
@ -60,8 +67,6 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware @@ -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 @@ -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);
}
/**

5
src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java

@ -74,12 +74,11 @@ class ProjectingMethodInterceptorUnitTests { @@ -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();
}

Loading…
Cancel
Save