Skip to content

Commit bd3992d

Browse files
mp911dechristophstrobl
authored andcommitted
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
1 parent 04f59f1 commit bd3992d

File tree

4 files changed

+65
-15
lines changed

4 files changed

+65
-15
lines changed

src/main/asciidoc/repository-projections.adoc

+24
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,30 @@ interface NamesOnly {
197197

198198
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>>.
199199

200+
[[projections.interfaces.nullable-wrappers]]
201+
=== Nullable Wrappers
202+
203+
Getters in projection interfaces can make use of nullable wrappers for improved null-safety. Currently supported wrapper types are:
204+
205+
* `java.util.Optional`
206+
* `com.google.common.base.Optional`
207+
* `scala.Option`
208+
* `io.vavr.control.Option`
209+
210+
.A projection interface using nullable wrappers
211+
====
212+
[source, java]
213+
----
214+
interface NamesOnly {
215+
216+
Optional<String> getFirstname();
217+
}
218+
----
219+
====
220+
221+
If the underlying projection value is not `null`, then values are returned using the present-representation of the wrapper type.
222+
In case the backing value is `null`, then the getter method returns the empty representation of the used wrapper type.
223+
200224
[[projections.dtos]]
201225
== Class-based Projections (DTOs)
202226

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

+29-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.springframework.core.CollectionFactory;
3232
import org.springframework.core.convert.ConversionService;
3333
import org.springframework.data.util.ClassTypeInformation;
34+
import org.springframework.data.util.NullableWrapper;
35+
import org.springframework.data.util.NullableWrapperConverters;
3436
import org.springframework.data.util.TypeInformation;
3537
import org.springframework.lang.Nullable;
3638
import org.springframework.util.Assert;
@@ -67,23 +69,43 @@ class ProjectingMethodInterceptor implements MethodInterceptor {
6769
@Override
6870
public Object invoke(@SuppressWarnings("null") @Nonnull MethodInvocation invocation) throws Throwable {
6971

72+
TypeInformation<?> type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod());
73+
TypeInformation<?> resultType = type;
74+
TypeInformation<?> typeToReturn = type;
75+
7076
Object result = delegate.invoke(invocation);
77+
boolean applyWrapper = false;
78+
79+
if (NullableWrapperConverters.supports(type.getType())
80+
&& (result == null || !NullableWrapperConverters.supports(result.getClass()))) {
81+
resultType = NullableWrapperConverters.unwrapActualType(typeToReturn);
82+
applyWrapper = true;
83+
}
84+
85+
result = potentiallyConvertResult(resultType, result);
86+
87+
if (applyWrapper) {
88+
return conversionService.convert(new NullableWrapper(result), typeToReturn.getType());
89+
}
90+
91+
return result;
92+
}
93+
94+
@Nullable
95+
protected Object potentiallyConvertResult(TypeInformation<?> type, @Nullable Object result) {
7196

7297
if (result == null) {
7398
return null;
7499
}
75100

76-
TypeInformation<?> type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod());
77-
Class<?> rawType = type.getType();
78-
79-
if (type.isCollectionLike() && !ClassUtils.isPrimitiveArray(rawType)) {
101+
if (type.isCollectionLike() && !ClassUtils.isPrimitiveArray(type.getType())) {
80102
return projectCollectionElements(asCollection(result), type);
81103
} else if (type.isMap()) {
82104
return projectMapValues((Map<?, ?>) result, type);
83-
} else if (conversionRequiredAndPossible(result, rawType)) {
84-
return conversionService.convert(result, rawType);
105+
} else if (conversionRequiredAndPossible(result, type.getType())) {
106+
return conversionService.convert(result, type.getType());
85107
} else {
86-
return getProjection(result, rawType);
108+
return getProjection(result, type.getType());
87109
}
88110
}
89111

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

+10-5
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
import org.springframework.aop.framework.Advised;
2727
import org.springframework.aop.framework.ProxyFactory;
2828
import org.springframework.beans.factory.BeanClassLoaderAware;
29-
import org.springframework.core.convert.ConversionService;
3029
import org.springframework.core.convert.support.DefaultConversionService;
30+
import org.springframework.core.convert.support.GenericConversionService;
31+
import org.springframework.data.util.NullableWrapperConverters;
3132
import org.springframework.lang.Nullable;
3233
import org.springframework.util.Assert;
3334
import org.springframework.util.ClassUtils;
@@ -47,8 +48,14 @@
4748
*/
4849
class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware {
4950

51+
final static GenericConversionService CONVERSION_SERVICE = new DefaultConversionService();
52+
53+
static {
54+
NullableWrapperConverters.registerConvertersIn(CONVERSION_SERVICE);
55+
CONVERSION_SERVICE.removeConvertible(Object.class, Object.class);
56+
}
57+
5058
private final List<MethodInterceptorFactory> factories;
51-
private final ConversionService conversionService;
5259
private final Map<Class<?>, ProjectionInformation> projectionInformationCache = new ConcurrentReferenceHashMap<>();
5360
private @Nullable ClassLoader classLoader;
5461

@@ -60,8 +67,6 @@ protected ProxyProjectionFactory() {
6067
this.factories = new ArrayList<>();
6168
this.factories.add(MapAccessingMethodInterceptorFactory.INSTANCE);
6269
this.factories.add(PropertyAccessingMethodInvokerFactory.INSTANCE);
63-
64-
this.conversionService = DefaultConversionService.getSharedInstance();
6570
}
6671

6772
/*
@@ -174,7 +179,7 @@ private MethodInterceptor getMethodInterceptor(Object source, Class<?> projectio
174179
.createMethodInterceptor(source, projectionType);
175180

176181
return new ProjectingMethodInterceptor(this,
177-
postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType), conversionService);
182+
postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType), CONVERSION_SERVICE);
178183
}
179184

180185
/**

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,11 @@ void retunsDelegateResultAsIsIfTypesMatch() throws Throwable {
7474
assertThat(methodInterceptor.invoke(invocation)).isEqualTo("Foo");
7575
}
7676

77-
@Test // DATAREST-221
77+
@Test // DATAREST-221, DATACMNS-1762
7878
void returnsNullAsIs() throws Throwable {
7979

8080
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor, conversionService);
81-
82-
when(interceptor.invoke(invocation)).thenReturn(null);
81+
mockInvocationOf("getString", null);
8382

8483
assertThat(methodInterceptor.invoke(invocation)).isNull();
8584
}

0 commit comments

Comments
 (0)