diff --git a/build.gradle b/build.gradle index e80719423..365d3a782 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } } diff --git a/gradle.properties b/gradle.properties index e35f31c16..cffdfa76b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/spring-data-rest-webmvc/build.gradle b/spring-data-rest-webmvc/build.gradle index 824707db9..69fc25827 100644 --- a/spring-data-rest-webmvc/build.gradle +++ b/spring-data-rest-webmvc/build.gradle @@ -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" diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java index 844a56467..0ead69b1e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java @@ -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; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcePostProcessorInvokingHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcePostProcessorInvokingHandlerAdapter.java new file mode 100644 index 000000000..b14b41789 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcePostProcessorInvokingHandlerAdapter.java @@ -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}. + *

+ * 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>> resourceProcessors = new ArrayList>>(); + + @Autowired(required = false) + private List>> resourcesProcessors = new ArrayList>>(); + + /* + * (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 newHandlers = new ArrayList(); + newHandlers.add(new ResourceProcessingHandlerMethodReturnValueHandler(oldHandlers, resourceProcessors, + resourcesProcessors)); + + // Configure the new handler to be used + this.setReturnValueHandlers(newHandlers); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessingHandlerMethodReturnValueHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessingHandlerMethodReturnValueHandler.java new file mode 100644 index 000000000..7e8805008 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessingHandlerMethodReturnValueHandler.java @@ -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> RESOURCE_TYPES = new HashSet>(2, 1); + private static final Set> PARAMETER_TYPES = new HashSet>(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 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> resourceProcessors, List> 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(); + + for (ResourceProcessor> processor : resourceProcessors) { + this.processorWrapper.add(new ResourceProcessorWrapper(processor)); + } + + for (ResourcesProcessor> 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 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(newBody, source.getHeaders(), source.getStatusCode()); + } else { + HttpEntity source = (HttpEntity) originalValue; + return new HttpEntity(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> 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> 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>) 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> 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> 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>) 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); + } + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ResourceProcessingHandlerMethodReturnValueHandlerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ResourceProcessingHandlerMethodReturnValueHandlerUnitTests.java new file mode 100644 index 000000000..a00014f1a --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ResourceProcessingHandlerMethodReturnValueHandlerUnitTests.java @@ -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> resourceProcessors; + List> resourcesProcessors; + + Resource source = new Resource("foo"); + Resource result = StringResourceProcessor.RESULT; + + @Before + public void setUp() { + resourceProcessors = new ArrayList>(); + resourcesProcessors = new ArrayList>(); + } + + @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> input = new HttpEntity>(source); + HttpEntity> output = new HttpEntity>(result); + + assertProcessorInvokedForMethod("stringResourceEntity", input, output); + assertProcessorInvokedForMethod("resourceEntity", input, output); + } + + @Test + public void invokesStringPostProcessorForSimpleStringResourceInResponseEntity() throws Exception { + + resourceProcessors.add(new StringResourceProcessor()); + resourceProcessors.add(new LongResourceProcessor()); + + ResponseEntity> input = new ResponseEntity>(source, HttpStatus.OK); + ResponseEntity> output = new ResponseEntity>(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> sources = new Resources>(Collections.singleton(source)); + + HttpEntity>> input = new HttpEntity>>(sources); + HttpEntity>> output = new HttpEntity>>( + 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> stringOutput = new HttpEntity>(result); + HttpEntity specializedInput = new HttpEntity(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> input = new HttpEntity>(source); + + assertProcessorInvokedForMethod("stringResourceEntity", input, input); + } + + @Test + public void invokesSpecializedStringPostProcessor() throws Exception { + + resourceProcessors.add(new SpecializedStringResourceProcessor()); + resourceProcessors.add(new LongResourceProcessor()); + + HttpEntity input = new HttpEntity(new StringResource("foo")); + HttpEntity output = new HttpEntity(SpecializedStringResourceProcessor.RESULT); + + assertProcessorInvokedForMethod("specializedStringResourceEntity", input, output); + } + + @Test + public void invokesLongPostProcessorForLongResource() throws Exception { + + resourceProcessors.add(new StringResourceProcessor()); + resourceProcessors.add(new LongResourceProcessor()); + + HttpEntity> input = new HttpEntity>(new Resource(50L)); + HttpEntity specializedInput = new HttpEntity(new LongResource(50L)); + HttpEntity> output = new HttpEntity>(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> resources(); + + Resource resource(); + + StringResource specializedResource(); + + Object object(); + + HttpEntity> resourceEntity(); + + HttpEntity> resourcesEntity(); + + HttpEntity objectEntity(); + + HttpEntity> stringResourceEntity(); + + HttpEntity> numberResourceEntity(); + + HttpEntity specializedStringResourceEntity(); + + ResponseEntity> resourceResponseEntity(); + + ResponseEntity> resourcesResponseEntity(); + } + + /** + * {@link ResourceProcessor} to process {@link String}s. + * + * @author Oliver Gierke + */ + static class StringResourceProcessor implements ResourceProcessor> { + + static final Resource RESULT = new Resource("bar"); + + @Override + public Resource process(Resource resource) { + return RESULT; + } + } + + static class StringResourcesProcessor implements ResourcesProcessor>> { + + static final Resources> RESULT = new Resources>( + Collections.singleton(StringResourceProcessor.RESULT)); + + @Override + public Resources process(Resources> resources) { + return RESULT; + } + } + + /** + * {@link ResourceProcessor} to process {@link Long} values. + * + * @author Oliver Gierke + */ + static class LongResourceProcessor implements ResourceProcessor> { + + static final Resource RESULT = new Resource(10L); + + @Override + public Resource process(Resource resource) { + return RESULT; + } + } + + static class StringResource extends Resource { + + public StringResource(String value) { + super(value); + } + } + + static class LongResource extends Resource { + + public LongResource(Long value) { + super(value); + } + } + + static class SpecializedStringResourceProcessor implements ResourceProcessor { + + static final StringResource RESULT = new StringResource("foobar"); + + @Override + public Resource process(StringResource resource) { + return RESULT; + } + } +}