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
This commit is contained in:
committed by
Christoph Strobl
parent
04f59f1307
commit
bd3992dfc5
@@ -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)
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user