diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptor.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptor.java index 712eddc42..317e7dbc8 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptor.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptor.java @@ -15,8 +15,17 @@ */ package org.springframework.data.rest.core.projection; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; + import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.CollectionFactory; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -60,9 +69,77 @@ class ProjectingMethodInterceptor implements MethodInterceptor { return null; } - Class returnType = invocation.getMethod().getReturnType(); + TypeInformation type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod()); + if (type.isCollectionLike()) { + return projectCollectionElements(asCollection(result), type); + } else if (type.isMap()) { + return projectMapValues((Map) result, type); + } else { + return getProjection(result, type.getType()); + } + } + + /** + * Creates projections of the given {@link Collection}'s elements if necessary and returns a new collection containing + * the projection results. + * + * @param sources must not be {@literal null}. + * @param type must not be {@literal null}. + * @return + */ + private Collection projectCollectionElements(Collection sources, TypeInformation type) { + + Collection result = CollectionFactory.createCollection(type.getType(), sources.size()); + + for (Object source : sources) { + result.add(getProjection(source, type.getComponentType().getType())); + } + + return result; + } + + /** + * Creates projections of the given {@link Map}'s values if necessary and returns an new {@link Map} with the handled + * values. + * + * @param sources must not be {@literal null}. + * @param type must not be {@literal null}. + * @return + */ + private Map projectMapValues(Map sources, TypeInformation type) { + + Map result = CollectionFactory.createMap(type.getType(), sources.size()); + + for (Entry source : sources.entrySet()) { + result.put(source.getKey(), getProjection(source.getValue(), type.getMapValueType().getType())); + } + + return result; + } + + private Object getProjection(Object result, Class returnType) { return ClassUtils.isAssignable(returnType, result.getClass()) ? result : factory.createProjection(result, returnType); } + + /** + * Turns the given value into a {@link Collection}. Will create an empty {@link Collection} for {@literal null}, turn + * an array iinto a collection an wrap all other values into a single-element collection. + * + * @param source can be {@literal null}. + * @return + */ + private static Collection asCollection(Object source) { + + if (source == null) { + return Collections.emptySet(); + } else if (source instanceof Collection) { + return (Collection) source; + } else if (source.getClass().isArray()) { + return Arrays.asList((Object[]) source); + } else { + return Collections.singleton(source); + } + } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptorUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptorUnitTests.java index 7232938a4..bfccc110f 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptorUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptorUnitTests.java @@ -20,8 +20,16 @@ import static org.junit.Assert.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -31,6 +39,7 @@ import org.mockito.runners.MockitoJUnitRunner; * Unit tests for {@link ProjectingMethodInterceptor}. * * @author Oliver Gierke + * @author Saulo Medeiros de Araujo */ @RunWith(MockitoJUnitRunner.class) public class ProjectingMethodInterceptorUnitTests { @@ -95,6 +104,96 @@ public class ProjectingMethodInterceptorUnitTests { verify(factory, times(0)).createProjection(anyObject(), (Class) anyObject()); } + /** + * @see DATAREST-394, DATAREST-408 + */ + @Test + @SuppressWarnings("unchecked") + public void appliesProjectionToNonEmptySets() throws Throwable { + + MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(null), interceptor); + Object result = methodInterceptor.invoke(mockInvocationOf("getHelperCollection", + Collections.singleton(mock(Helper.class)))); + + assertThat(result, is(instanceOf(Set.class))); + + Set projections = (Set) result; + assertThat(projections, hasSize(1)); + assertThat(projections, hasItem(instanceOf(HelperProjection.class))); + } + + /** + * @see DATAREST-394, DATAREST-408 + */ + @Test + @SuppressWarnings("unchecked") + public void appliesProjectionToNonEmptyLists() throws Throwable { + + MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(null), interceptor); + Object result = methodInterceptor.invoke(mockInvocationOf("getHelperList", + Collections.singletonList(mock(Helper.class)))); + + assertThat(result, is(instanceOf(List.class))); + + List projections = (List) result; + + assertThat(projections, hasSize(1)); + assertThat(projections, hasItem(instanceOf(HelperProjection.class))); + } + + /** + * @see DATAREST-394, DATAREST-408 + */ + @Test + @SuppressWarnings("unchecked") + public void allowsMaskingAnArrayIntoACollection() throws Throwable { + + MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(null), interceptor); + Object result = methodInterceptor.invoke(mockInvocationOf("getHelperArray", new Helper[] { mock(Helper.class) })); + + assertThat(result, is(instanceOf(Collection.class))); + + Collection projections = (Collection) result; + + assertThat(projections, hasSize(1)); + assertThat(projections, hasItem(instanceOf(HelperProjection.class))); + } + + /** + * @see DATAREST-394, DATAREST-408 + */ + @Test + @SuppressWarnings("unchecked") + public void appliesProjectionToNonEmptyMap() throws Throwable { + + MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(null), interceptor); + + Object result = methodInterceptor.invoke(mockInvocationOf("getHelperMap", + Collections.singletonMap("foo", mock(Helper.class)))); + + assertThat(result, is(instanceOf(Map.class))); + + Map projections = (Map) result; + assertThat(projections.entrySet(), is(Matchers.> iterableWithSize(1))); + assertThat(projections, hasEntry(is("foo"), instanceOf(HelperProjection.class))); + } + + /** + * Mocks the {@link Helper} method of the given name to return the given value. + * + * @param methodName + * @param returnValue + * @return + * @throws Throwable + */ + private MethodInvocation mockInvocationOf(String methodName, Object returnValue) throws Throwable { + + when(invocation.getMethod()).thenReturn(Helper.class.getMethod(methodName)); + when(interceptor.invoke(invocation)).thenReturn(returnValue); + + return invocation; + } + interface Helper { Helper getHelper(); @@ -102,5 +201,21 @@ public class ProjectingMethodInterceptorUnitTests { String getString(); long getPrimitive(); + + Collection getHelperCollection(); + + List getHelperList(); + + Set getHelperSet(); + + Map getHelperMap(); + + Collection getHelperArray(); + } + + interface HelperProjection { + Helper getHelper(); + + String getString(); } }