From cdeecd4a75f6944efa4795bb592d9b75f7c9b160 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 2 Feb 2021 16:51:40 +0100 Subject: [PATCH] Throw UnsupportedOperationException when a projected value cannot be returned. We now throw UnsupportedOperationException when a projected value cannot be returned because it cannot be brought into the target type, either via conversion or projection. This exception improves the error message by avoiding throwing IllegalArgumentException: Projection type must be an interface from the last branch that falls back into projections. Closes #2290. Original Pull Request: #2291 --- .../ProjectingMethodInterceptor.java | 10 ++++++++-- .../ProjectingMethodInterceptorUnitTests.java | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java index ae75bf338..9683d7b58 100644 --- a/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java @@ -104,8 +104,14 @@ class ProjectingMethodInterceptor implements MethodInterceptor { return projectMapValues((Map) result, type); } else if (conversionRequiredAndPossible(result, type.getType())) { return conversionService.convert(result, type.getType()); - } else { + } else if (ClassUtils.isAssignable(type.getType(), result.getClass())) { + return result; + } else if (type.getType().isInterface()) { return getProjection(result, type.getType()); + } else { + throw new UnsupportedOperationException(String.format( + "Cannot convert value '%s' of type '%s' to '%s' and cannot create a projection as the target type is not an interface", + result, ClassUtils.getDescriptiveType(result), ClassUtils.getQualifiedName(type.getType()))); } } @@ -155,7 +161,7 @@ class ProjectingMethodInterceptor implements MethodInterceptor { } @Nullable - private Object getProjection(Object result, Class returnType) { + private Object getProjection(@Nullable Object result, Class returnType) { return result == null || ClassUtils.isAssignable(returnType, result.getClass()) ? result : factory.createProjection(returnType, result); } diff --git a/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java index f1943ef45..982000aba 100755 --- a/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.math.BigInteger; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -95,6 +96,21 @@ class ProjectingMethodInterceptorUnitTests { verify(factory, times(0)).createProjection((Class) any(), any()); } + @Test // #2290 + void failsForNonConvertibleTypes() throws Throwable { + + MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor, conversionService); + + when(invocation.getMethod()).thenReturn(Helper.class.getMethod("getBoolean")); + when(interceptor.invoke(invocation)).thenReturn(BigInteger.valueOf(1)); + + assertThatThrownBy(() -> methodInterceptor.invoke(invocation)) // + .isInstanceOf(UnsupportedOperationException.class) // + .hasMessageContaining("'1'") // + .hasMessageContaining("BigInteger") // + .hasMessageContaining("boolean"); + } + @Test // DATAREST-394, DATAREST-408 @SuppressWarnings("unchecked") void appliesProjectionToNonEmptySets() throws Throwable { @@ -213,6 +229,8 @@ class ProjectingMethodInterceptorUnitTests { long getPrimitive(); + boolean getBoolean(); + Collection getHelperCollection(); List getHelperList();