DATAREST-394, DATAREST-408 - Projections now consider collections and maps.

The ProjectingMethodInterceptor now inspects collections and maps to apply projections to collection elements and map values. Test cases inspired by a contribution of Saulo Medeiros de Araujo.

Related pull request: #152.
This commit is contained in:
Oliver Gierke
2014-09-26 12:06:30 -03:00
committed by Oliver Gierke
parent 42b906ca04
commit e6a19a366d
2 changed files with 193 additions and 1 deletions

View File

@@ -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<Object> projectCollectionElements(Collection<?> sources, TypeInformation<?> type) {
Collection<Object> 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<Object, Object> projectMapValues(Map<?, ?> sources, TypeInformation<?> type) {
Map<Object, Object> 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);
}
}
}

View File

@@ -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<Object> projections = (Set<Object>) 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<Object> projections = (List<Object>) 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<Object> projections = (Collection<Object>) 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<String, Object> projections = (Map<String, Object>) result;
assertThat(projections.entrySet(), is(Matchers.<Entry<String, Object>> 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<HelperProjection> getHelperCollection();
List<HelperProjection> getHelperList();
Set<HelperProjection> getHelperSet();
Map<String, HelperProjection> getHelperMap();
Collection<HelperProjection> getHelperArray();
}
interface HelperProjection {
Helper getHelper();
String getString();
}
}