DATAREST-40 - General purpose post-processing mechanism for resources.

This commit implements the components described in DATAREST-40. The core
of it is the ResourceProcessingHandlerMethodReturnValueHandler. It gets 
ResourceProcessor and ResourcesProcessor instances handed into its 
constructor and selects the ones that need to be invoked in the calls to
handleReturnValue(…). In this process it does a variety of generics 
checks to ensure only the correct post-processors. To do so it inspects 
the controller method's return type, the object value type as well as 
the returned value potentially.

Assume we have a specialized Resource type 
StringResource extends Resource<String>. Beyond that we have a 
ResourceProcessor<Resource<String>> registered. Here's the decision 
matrix:

Return type					Returned type		What get's used?
Resource<?>					Resource<String>	object value inspected (Resource.getContent())
Resource<String>			Resource<String>	method return type
Resource<String>			StringResource		object value type as it's more specialized
StringResource				StringResource		method return type
Resource<?>					Resource<Long>		no invocation, object value doesn't match
Resource<? extends Number>	Resource<Long>		no invocation, object value doesn't match
Resource<Long>				Resource<Long>		no invocation, object value doesn't match

This works exactly the same for Resources returned. We peek into the 
collection returned by getContent() in case return value inspection is 
needed for type matches. If the collection is empty we don't match. The 
same applies to null values returned.

The second component added is the ResourcePostProcessorInvokingHandlerAdapter 
which customizes the setup of a RequestMappingHandlerAdapter by fronting 
the current configuration with the 
ResourceProcessingHandlerMethodReturnValueHandler to let it intercept
the handling and potentially call the post-processors.

Upgraded to snapshots of Spring Data Commons until we get to the next 
release and added Mockito as dependency.
This commit is contained in:
Oliver Gierke
2012-08-22 16:27:08 +02:00
parent 9069774dc5
commit ed9bb157c7
7 changed files with 820 additions and 5 deletions

View File

@@ -15,7 +15,7 @@ allprojects {
}
repositories {
//maven { url "http://repo.springsource.org/libs-snapshot" }
maven { url "http://repo.springsource.org/libs-snapshot" }
maven { url "http://repo.springsource.org/libs-milestone" }
maven { url "http://repo.springsource.org/libs-release" }
}
@@ -83,7 +83,7 @@ configure(subprojects) { subproject ->
testCompile "org.hamcrest:hamcrest-library:1.2.1"
testCompile "org.springframework:spring-test:$springVersion"
testRuntime "org.springframework:spring-context-support:$springVersion"
testCompile "org.mockito:mockito-core:1.8.5"
}
}

View File

@@ -10,8 +10,8 @@ cglibVersion = 2.2.2
groovyVersion = 1.8.6
# Supporting libraries
sdCommonsVersion = 1.3.2.RELEASE
sdJpaVersion = 1.2.0.M1
sdCommonsVersion = 1.4.0.BUILD-SNAPSHOT
sdJpaVersion = 1.2.0.BUILD-SNAPSHOT
jacksonVersion = 1.9.7
hibernateVersion = 4.1.4.Final

View File

@@ -15,6 +15,9 @@ dependencies {
// Repository Exporter support
compile project(":spring-data-rest-repository")
compile "org.springframework.hateoas:spring-hateoas:0.2.0.BUILD-SNAPSHOT"
compile "org.springframework.data:spring-data-commons-core:$sdCommonsVersion"
runtime "org.hibernate:hibernate-entitymanager:$hibernateVersion"
runtime "org.hsqldb:hsqldb:2.2.8"

View File

@@ -16,7 +16,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
*
* @author Jon Brisbin
*/
public class RepositoryRestHandlerAdapter extends RequestMappingHandlerAdapter {
public class RepositoryRestHandlerAdapter extends ResourcePostProcessorInvokingHandlerAdapter {
@Autowired
private ResourcesReturnValueHandler resourcesReturnValueHandler;

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.ResourcesProcessor;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
/**
* Special {@link RequestMappingHandlerAdapter} that tweaks the {@link HandlerMethodReturnValueHandlerComposite} to be
* proxied by a {@link ResourceProcessingHandlerMethodReturnValueHandler} which will invoke the
* {@link ResourceProcessor}s/{@link ResourcesProcessor}s found in the application context and eventually delegate to
* the originally configured {@link HandlerMethodReturnValueHandler}.
* <p>
* This is a separate component as it might make sense to deploy it in a standalone SpringMVC application to enable post
* processing. It would actually make most sense in Spring HATEOAS project.
*
* @author Oliver Gierke
*/
public class ResourcePostProcessorInvokingHandlerAdapter extends RequestMappingHandlerAdapter {
@Autowired(required = false)
private List<ResourceProcessor<? extends Resource<?>>> resourceProcessors = new ArrayList<ResourceProcessor<? extends Resource<?>>>();
@Autowired(required = false)
private List<ResourcesProcessor<? extends Resources<?>>> resourcesProcessors = new ArrayList<ResourcesProcessor<? extends Resources<?>>>();
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
// Retrieve actual handlers to use as delegate
HandlerMethodReturnValueHandlerComposite oldHandlers = getReturnValueHandlers();
// Set up ResourceProcessingHandlerMethodResolver to delegate to originally configured ones
List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
newHandlers.add(new ResourceProcessingHandlerMethodReturnValueHandler(oldHandlers, resourceProcessors,
resourcesProcessors));
// Configure the new handler to be used
this.setReturnValueHandlers(newHandlers);
}
}

View File

@@ -0,0 +1,408 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
import static org.springframework.data.util.ClassTypeInformation.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.ResourcesProcessor;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* {@link HandlerMethodReturnValueHandler} to post-process the objects returned from controller methods using the
* configured {@link ResourceProcessor}s / {@link ResourcesProcessor}s.
*
* @author Oliver Gierke
*/
public class ResourceProcessingHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
private static final Set<Class<?>> RESOURCE_TYPES = new HashSet<Class<?>>(2, 1);
private static final Set<Class<?>> PARAMETER_TYPES = new HashSet<Class<?>>(3, 1);
static {
RESOURCE_TYPES.add(Resource.class);
RESOURCE_TYPES.add(Resources.class);
PARAMETER_TYPES.addAll(RESOURCE_TYPES);
PARAMETER_TYPES.add(HttpEntity.class);
}
private final HandlerMethodReturnValueHandler delegate;
private final List<PostProcessorWrapper> processorWrapper;
/**
* Creates a new {@link ResourceProcessingHandlerMethodReturnValueHandler} using the given delegate to eventually
* delegate calls to {@link #handleReturnValue(Object, MethodParameter, ModelAndViewContainer, NativeWebRequest)} to.
* Will consider the given {@link ResourceProcessor} / {@link ResourcesProcessor} to post-process the controller
* methods return value to before invoking the delegate.
*
* @param delegate the {@link HandlerMethodReturnValueHandler} to evenually delegate calls to, must not be
* {@literal null}.
* @param resourceProcessors the {@link ResourceProcessor}s to be considered, must not be {@literal null}.
* @param resourcesProcessors the {@link ResourcesProcessor}s to be considered, must not be {@literal null}.
*/
@Autowired
public ResourceProcessingHandlerMethodReturnValueHandler(HandlerMethodReturnValueHandler delegate,
List<ResourceProcessor<?>> resourceProcessors, List<ResourcesProcessor<?>> resourcesProcessors) {
Assert.notNull(delegate, "Delegate must not be null!");
Assert.notNull(resourceProcessors, "ResourceProcessors must not be null!");
Assert.notNull(resourcesProcessors, "ResourcesProcessors must not be null!");
this.delegate = delegate;
this.processorWrapper = new ArrayList<PostProcessorWrapper>();
for (ResourceProcessor<? extends Resource<?>> processor : resourceProcessors) {
this.processorWrapper.add(new ResourceProcessorWrapper(processor));
}
for (ResourcesProcessor<? extends Resources<?>> processor : resourcesProcessors) {
this.processorWrapper.add(new ResourcesProcessorWrapper(processor));
}
Collections.sort(this.processorWrapper, AnnotationAwareOrderComparator.INSTANCE);
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodReturnValueHandler#supportsReturnType(org.springframework.core.MethodParameter)
*/
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return delegate.supportsReturnType(returnType);
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodReturnValueHandler#handleReturnValue(java.lang.Object, org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest)
*/
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
Object value = returnValue;
if (returnValue instanceof HttpEntity) {
value = ((HttpEntity<?>) returnValue).getBody();
}
// No post-processable type found - proceed with delegate
if (!isResourceType(value)) {
delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
return;
}
// We have a Resource or Resources - find suitable processors
TypeInformation<?> targetType = ClassTypeInformation.fromReturnTypeOf(returnType.getMethod());
// Unbox HttpEntity
if (HttpEntity.class.isAssignableFrom(targetType.getType())) {
targetType = targetType.getTypeArguments().get(0);
}
TypeInformation<? extends Object> returnValueTypeInformation = ClassTypeInformation.from(value.getClass());
// Returned value is actually of a more specific type, use this type information
if (!targetType.getType().equals(returnValueTypeInformation.getType())) {
targetType = returnValueTypeInformation;
}
Object processedValue = value;
for (PostProcessorWrapper wrapper : this.processorWrapper) {
if (wrapper.supports(targetType, processedValue)) {
processedValue = wrapper.invokeProcessor(processedValue);
}
}
delegate.handleReturnValue(rewrapResult(processedValue, returnValue), returnType, mavContainer, webRequest);
}
/**
* Returns whether the given value is a resource (i.e. implements {@link Resource) or {@link Resources}).
*
* @param value
* @return
*/
private boolean isResourceType(Object value) {
if (value == null) {
return false;
}
for (Class<?> type : RESOURCE_TYPES) {
if (type.isAssignableFrom(value.getClass())) {
return true;
}
}
return false;
}
/**
* Re-wraps the result of the post-processing work into an {@link HttpEntity} or {@link ResponseEntity} if the
* original value was one of those two types. Copies headers and status code from the original value but uses the new
* body.
*
* @param newBody the post-processed value.
* @param originalValue the original input value.
* @return
*/
static Object rewrapResult(Object newBody, Object originalValue) {
if (!(originalValue instanceof HttpEntity)) {
return newBody;
}
if (originalValue instanceof ResponseEntity) {
ResponseEntity<?> source = (ResponseEntity<?>) originalValue;
return new ResponseEntity<Object>(newBody, source.getHeaders(), source.getStatusCode());
} else {
HttpEntity<?> source = (HttpEntity<?>) originalValue;
return new HttpEntity<Object>(newBody, source.getHeaders());
}
}
/**
* Interface to unify interaction with {@link ResourceProcessor} and {@link ResourcesProcessor}. The {@link Ordered}
* rank should be determined by the underlying processor.
*
* @author Oliver Gierke
*/
private interface PostProcessorWrapper extends Ordered {
/**
* Returns whether the underlying processor supports the given {@link TypeInformation}. It might also aditionally
* inspect the object that would eventually be handed to the processor.
*
* @param typeInformation the type of object to be post processed, will never be {@literal null}.
* @param value the object that would be passed into the processor eventually, can be {@literal null}.
* @return
*/
boolean supports(TypeInformation<?> typeInformation, Object value);
/**
* Performs the actual invocation of the processor. Implementations can be sure
* {@link #supports(TypeInformation, Object)} has been called before and returned {@literal true}.
*
* @param object
* @return
*/
Object invokeProcessor(Object object);
}
/**
* {@link PostProcessorWrapper} to invoke {@link ResourceProcessor} instances.
*
* @author Oliver Gierke
*/
private static class ResourceProcessorWrapper implements PostProcessorWrapper {
private final ResourceProcessor<? extends Resource<?>> processor;
private final TypeInformation<?> targetType;
/**
* Creates a new {@link ResourceProcessorWrapper} for the given {@link ResourceProcessor}.
*
* @param processor must not be {@literal null}.
*/
public ResourceProcessorWrapper(ResourceProcessor<? extends Resource<?>> processor) {
Assert.notNull(processor);
this.processor = processor;
this.targetType = from(processor.getClass()).getSuperTypeInformation(ResourceProcessor.class).getComponentType();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.ResourceProcessingHandlerMethodReturnValueHandler.PostProcessorWrapper#supports(org.springframework.data.util.TypeInformation, java.lang.Object)
*/
@Override
public boolean supports(TypeInformation<?> typeInformation, Object value) {
if (value == null || !Resource.class.isAssignableFrom(value.getClass())) {
return false;
}
return targetType.isAssignableFrom(typeInformation) || isValueTypeMatch((Resource<?>) value, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.ResourceProcessingHandlerMethodReturnValueHandler.PostProcessorWrapper#invokeProcessor(java.lang.Object)
*/
@Override
@SuppressWarnings("unchecked")
public Object invokeProcessor(Object object) {
return ((ResourceProcessor<Resource<?>>) processor).process((Resource<?>) object);
}
/*
* (non-Javadoc)
* @see org.springframework.core.Ordered#getOrder()
*/
@Override
public int getOrder() {
return CustomOrderAwareComparator.INSTANCE.getOrder(processor);
}
/**
* Returns whether the given {@link Resource} matches the given target {@link TypeInformation}. We inspect the
* {@link Resource}'s value to determine the match.
*
* @param resource
* @param target must not be {@literal null}.
* @return whether the given {@link Resource} can be assigned to the given target {@link TypeInformation}
*/
private static boolean isValueTypeMatch(Resource<?> resource, TypeInformation<?> target) {
if (resource == null || !target.getType().equals(resource.getClass())) {
return false;
}
Object content = resource.getContent();
if (content == null) {
return false;
}
return target.getSuperTypeInformation(Resource.class).getComponentType().getType()
.isAssignableFrom(content.getClass());
}
}
/**
* {@link PostProcessorWrapper} for {@link ResourcesProcessor}s.
*
* @author Oliver Gierke
*/
private static class ResourcesProcessorWrapper implements PostProcessorWrapper {
private final ResourcesProcessor<? extends Resources<?>> processor;
private final TypeInformation<?> targetType;
/**
* Creates a new {@link ResourcesProcessorWrapper} for the given {@link ResourcesProcessor}.
*
* @param processor must not be {@literal null}.
*/
public ResourcesProcessorWrapper(ResourcesProcessor<? extends Resources<?>> processor) {
Assert.notNull(processor);
this.processor = processor;
this.targetType = from(processor.getClass()).getSuperTypeInformation(ResourcesProcessor.class).getComponentType();
}
/* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.PostProcessorWrapper#supports(org.springframework.data.util.TypeInformation, java.lang.Object)
*/
@Override
public boolean supports(TypeInformation<?> typeInformation, Object value) {
if (value == null || !Resources.class.isAssignableFrom(value.getClass())) {
return false;
}
return targetType.isAssignableFrom(typeInformation) || isValueTypeMatch((Resources<?>) value, targetType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.ResourceProcessingHandlerMethodReturnValueHandler.PostProcessorWrapper#invokeProcessor(java.lang.Object)
*/
@Override
@SuppressWarnings("unchecked")
public Object invokeProcessor(Object object) {
return ((ResourcesProcessor<Resources<?>>) processor).process((Resources<?>) object);
}
/*
* (non-Javadoc)
* @see org.springframework.core.Ordered#getOrder()
*/
@Override
public int getOrder() {
return CustomOrderAwareComparator.INSTANCE.getOrder(processor);
}
/**
* Returns whether the given {@link Resources} instance matches the given {@link TypeInformation}. We predict this
* by inspecting the first element of the content of the {@link Resources}.
*
* @param resources the {@link Resources} to inspect.
* @param target that target {@link TypeInformation}.
* @return
*/
private static boolean isValueTypeMatch(Resources<?> resources, TypeInformation<?> target) {
if (resources == null || !Resources.class.equals(resources.getClass())) {
return false;
}
Collection<?> content = resources.getContent();
if (content.isEmpty()) {
return false;
}
Object element = content.iterator().next();
if (!(element instanceof Resource)) {
return false;
}
TypeInformation<?> resourceTypeInformation = target.getSuperTypeInformation(Resources.class).getComponentType();
return ResourceProcessorWrapper.isValueTypeMatch((Resource<?>) element, resourceTypeInformation);
}
}
/**
* Helper extension of {@link AnnotationAwareOrderComparator} to make {@link #getOrder()} public to allow it being
* used in a standalone fashion.
*
* @author Oliver Gierke
*/
private static class CustomOrderAwareComparator extends AnnotationAwareOrderComparator {
public static CustomOrderAwareComparator INSTANCE = new CustomOrderAwareComparator();
@Override
protected int getOrder(Object obj) {
return super.getOrder(obj);
}
}
}

View File

@@ -0,0 +1,335 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.test.webmvc;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.internal.matchers.Equals;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.MethodParameter;
import org.springframework.data.rest.webmvc.ResourceProcessingHandlerMethodReturnValueHandler;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.ResourcesProcessor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class ResourceProcessingHandlerMethodReturnValueHandlerUnitTests {
@Mock
HandlerMethodReturnValueHandler delegate;
@Mock
MethodParameter parameter;
List<ResourceProcessor<?>> resourceProcessors;
List<ResourcesProcessor<?>> resourcesProcessors;
Resource<String> source = new Resource<String>("foo");
Resource<String> result = StringResourceProcessor.RESULT;
@Before
public void setUp() {
resourceProcessors = new ArrayList<ResourceProcessor<?>>();
resourcesProcessors = new ArrayList<ResourcesProcessor<?>>();
}
@Test
public void supportsIfDelegateSupports() {
assertSupport(true);
}
@Test
public void doesNotSupportIfDelegateDoesNot() {
assertSupport(false);
}
private void assertSupport(boolean value) {
when(delegate.supportsReturnType(Mockito.any(MethodParameter.class))).thenReturn(value);
HandlerMethodReturnValueHandler handler = new ResourceProcessingHandlerMethodReturnValueHandler(delegate,
resourceProcessors, resourcesProcessors);
assertThat(handler.supportsReturnType(parameter), is(value));
}
@Test
public void invokesStringPostProcessorForSimpleStringResource() throws Exception {
resourceProcessors.add(new StringResourceProcessor());
resourceProcessors.add(new LongResourceProcessor());
HttpEntity<Resource<String>> input = new HttpEntity<Resource<String>>(source);
HttpEntity<Resource<String>> output = new HttpEntity<Resource<String>>(result);
assertProcessorInvokedForMethod("stringResourceEntity", input, output);
assertProcessorInvokedForMethod("resourceEntity", input, output);
}
@Test
public void invokesStringPostProcessorForSimpleStringResourceInResponseEntity() throws Exception {
resourceProcessors.add(new StringResourceProcessor());
resourceProcessors.add(new LongResourceProcessor());
ResponseEntity<Resource<String>> input = new ResponseEntity<Resource<String>>(source, HttpStatus.OK);
ResponseEntity<Resource<String>> output = new ResponseEntity<Resource<String>>(result, HttpStatus.OK);
assertProcessorInvokedForMethod("stringResourceEntity", input, output);
assertProcessorInvokedForMethod("resourceEntity", input, output);
}
@Test
public void invokesStringPostProcessorForSimpleStringResources() throws Exception {
resourceProcessors.add(new StringResourceProcessor());
resourceProcessors.add(new LongResourceProcessor());
resourcesProcessors.add(new StringResourcesProcessor());
Resources<Resource<String>> sources = new Resources<Resource<String>>(Collections.singleton(source));
HttpEntity<Resources<Resource<String>>> input = new HttpEntity<Resources<Resource<String>>>(sources);
HttpEntity<Resources<Resource<String>>> output = new HttpEntity<Resources<Resource<String>>>(
StringResourcesProcessor.RESULT);
assertProcessorInvokedForMethod("stringResourceEntity", input, output);
assertProcessorInvokedForMethod("resourceEntity", input, output);
}
@Test
public void invokesStringPostProcessorForSpecializedStringResource() throws Exception {
resourceProcessors.add(new StringResourceProcessor());
resourceProcessors.add(new LongResourceProcessor());
HttpEntity<Resource<String>> stringOutput = new HttpEntity<Resource<String>>(result);
HttpEntity<StringResource> specializedInput = new HttpEntity<StringResource>(new StringResource("foo"));
assertProcessorInvokedForMethod("stringResourceEntity", specializedInput, stringOutput);
assertProcessorInvokedForMethod("resourceEntity", specializedInput, stringOutput);
assertProcessorInvokedForMethod("specializedStringResourceEntity", specializedInput, stringOutput);
}
@Test
public void doesNotInvokeSpecializedStringPostProcessorForSimpleStringResource() throws Exception {
resourceProcessors.add(new SpecializedStringResourceProcessor());
resourceProcessors.add(new LongResourceProcessor());
HttpEntity<Resource<String>> input = new HttpEntity<Resource<String>>(source);
assertProcessorInvokedForMethod("stringResourceEntity", input, input);
}
@Test
public void invokesSpecializedStringPostProcessor() throws Exception {
resourceProcessors.add(new SpecializedStringResourceProcessor());
resourceProcessors.add(new LongResourceProcessor());
HttpEntity<StringResource> input = new HttpEntity<StringResource>(new StringResource("foo"));
HttpEntity<StringResource> output = new HttpEntity<StringResource>(SpecializedStringResourceProcessor.RESULT);
assertProcessorInvokedForMethod("specializedStringResourceEntity", input, output);
}
@Test
public void invokesLongPostProcessorForLongResource() throws Exception {
resourceProcessors.add(new StringResourceProcessor());
resourceProcessors.add(new LongResourceProcessor());
HttpEntity<Resource<Long>> input = new HttpEntity<Resource<Long>>(new Resource<Long>(50L));
HttpEntity<LongResource> specializedInput = new HttpEntity<LongResource>(new LongResource(50L));
HttpEntity<Resource<Long>> output = new HttpEntity<Resource<Long>>(LongResourceProcessor.RESULT);
assertProcessorInvokedForMethod("resourceEntity", specializedInput, output);
assertProcessorInvokedForMethod("numberResourceEntity", input, output);
}
private void assertProcessorInvokedForMethod(String methodName, Object returnValue, Object processedValue)
throws Exception {
HandlerMethodReturnValueHandler handler = new ResourceProcessingHandlerMethodReturnValueHandler(delegate,
resourceProcessors, resourcesProcessors);
Method method = Controller.class.getMethod(methodName);
MethodParameter returnType = new MethodParameter(method, -1);
handler.handleReturnValue(returnValue, returnType, null, null);
verify(delegate, times(1)).handleReturnValue(argThat(new HttpEntityMatcher(processedValue)), eq(returnType),
eq((ModelAndViewContainer) null), eq((NativeWebRequest) null));
}
@SuppressWarnings("serial")
static class HttpEntityMatcher extends Equals {
public HttpEntityMatcher(Object wanted) {
super(wanted);
}
/* (non-Javadoc)
* @see org.mockito.internal.matchers.Equals#matches(java.lang.Object)
*/
@Override
public boolean matches(Object actual) {
Object wanted = getWanted();
if (actual instanceof ResponseEntity && wanted instanceof ResponseEntity) {
ResponseEntity<?> left = (ResponseEntity<?>) wanted;
ResponseEntity<?> right = (ResponseEntity<?>) actual;
if (!left.getStatusCode().equals(right.getStatusCode())) {
return false;
}
}
if (actual instanceof HttpEntity && wanted instanceof HttpEntity) {
HttpEntity<?> left = (HttpEntity<?>) wanted;
HttpEntity<?> right = (HttpEntity<?>) actual;
if (!left.getBody().equals(right.getBody())) {
return false;
}
if (!left.getHeaders().equals(right.getHeaders())) {
return false;
}
return true;
}
return super.matches(actual);
}
}
interface Controller {
Resources<Resource<String>> resources();
Resource<String> resource();
StringResource specializedResource();
Object object();
HttpEntity<Resource<?>> resourceEntity();
HttpEntity<Resources<?>> resourcesEntity();
HttpEntity<Object> objectEntity();
HttpEntity<Resource<String>> stringResourceEntity();
HttpEntity<Resource<? extends Number>> numberResourceEntity();
HttpEntity<StringResource> specializedStringResourceEntity();
ResponseEntity<Resource<?>> resourceResponseEntity();
ResponseEntity<Resources<?>> resourcesResponseEntity();
}
/**
* {@link ResourceProcessor} to process {@link String}s.
*
* @author Oliver Gierke
*/
static class StringResourceProcessor implements ResourceProcessor<Resource<String>> {
static final Resource<String> RESULT = new Resource<String>("bar");
@Override
public Resource<String> process(Resource<String> resource) {
return RESULT;
}
}
static class StringResourcesProcessor implements ResourcesProcessor<Resources<Resource<String>>> {
static final Resources<Resource<String>> RESULT = new Resources<Resource<String>>(
Collections.singleton(StringResourceProcessor.RESULT));
@Override
public Resources<?> process(Resources<Resource<String>> resources) {
return RESULT;
}
}
/**
* {@link ResourceProcessor} to process {@link Long} values.
*
* @author Oliver Gierke
*/
static class LongResourceProcessor implements ResourceProcessor<Resource<Long>> {
static final Resource<Long> RESULT = new Resource<Long>(10L);
@Override
public Resource<?> process(Resource<Long> resource) {
return RESULT;
}
}
static class StringResource extends Resource<String> {
public StringResource(String value) {
super(value);
}
}
static class LongResource extends Resource<Long> {
public LongResource(Long value) {
super(value);
}
}
static class SpecializedStringResourceProcessor implements ResourceProcessor<StringResource> {
static final StringResource RESULT = new StringResource("foobar");
@Override
public Resource<?> process(StringResource resource) {
return RESULT;
}
}
}