Skip to content

DATACMNS-1762 - Support Optional wrapping for projection getters #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.4.0-SNAPSHOT</version>
<version>2.4.0-DATACMNS-1762-SNAPSHOT</version>

<name>Spring Data Core</name>

Expand Down
24 changes: 24 additions & 0 deletions src/main/asciidoc/repository-projections.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
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;
Expand Down Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
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;
Expand All @@ -47,8 +48,14 @@
*/
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;

Expand All @@ -60,8 +67,6 @@ protected ProxyProjectionFactory() {
this.factories = new ArrayList<>();
this.factories.add(MapAccessingMethodInterceptorFactory.INSTANCE);
this.factories.add(PropertyAccessingMethodInvokerFactory.INSTANCE);

this.conversionService = DefaultConversionService.getSharedInstance();
}

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

return new ProjectingMethodInterceptor(this,
postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType), conversionService);
postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType), CONVERSION_SERVICE);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.KotlinReflectionUtils;
Expand Down Expand Up @@ -105,7 +106,14 @@ public TypeInformation<?> getReturnType(Method method) {
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(java.lang.reflect.Method)
*/
public Class<?> getReturnedDomainClass(Method method) {
return QueryExecutionConverters.unwrapWrapperTypes(getReturnType(method)).getType();

TypeInformation<?> returnType = getReturnType(method);

if (ReactiveWrapperConverters.supports(returnType.getType())) {
return ReactiveWrapperConverters.unwrapWrapperTypes(returnType).getType();
}

return QueryExecutionConverters.unwrapWrapperTypes(returnType).getType();
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

Expand Down Expand Up @@ -299,8 +300,7 @@ private static boolean isNonUnwrappingWrapper(Class<?> parameterType) {

Assert.notNull(parameterType, "Parameter type must not be null!");

return QueryExecutionConverters.supports(parameterType)
&& !QueryExecutionConverters.supportsUnwrapping(parameterType);
return ReactiveWrappers.supports(parameterType);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.repository.util.NullableWrapper;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.NullableWrapper;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,41 +23,20 @@
* convert {@literal null} into an object of some sort.
*
* @author Oliver Gierke
* @author Mark Paluch
* @since 1.8
* @see QueryExecutionConverters
* @deprecated since 2.4, use {@link org.springframework.data.util.NullableWrapper} instead.
*/
public class NullableWrapper {

private final @Nullable Object value;
@Deprecated
public class NullableWrapper extends org.springframework.data.util.NullableWrapper {

/**
* Creates a new {@link NullableWrapper} for the given value.
*
* @param value can be {@literal null}.
*/
public NullableWrapper(@Nullable Object value) {
this.value = value;
}

/**
* Returns the type of the contained value. WIll fall back to {@link Object} in case the value is {@literal null}.
*
* @return will never be {@literal null}.
*/
public Class<?> getValueType() {

Object value = this.value;

return value == null ? Object.class : value.getClass();
}

/**
* Returns the backing value.
*
* @return the value can be {@literal null}.
*/
@Nullable
public Object getValue() {
return value;
super(value);
}
}
Loading