From ed9bb157c72f0d8861c2923d521bee0594a6dac8 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 22 Aug 2012 16:27:08 +0200 Subject: [PATCH] DATAREST-40 - General purpose post-processing mechanism for resources. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Beyond that we have a ResourceProcessor> registered. Here's the decision matrix: Return type Returned type What get's used? Resource Resource object value inspected (Resource.getContent()) Resource Resource method return type Resource StringResource object value type as it's more specialized StringResource StringResource method return type Resource Resource no invocation, object value doesn't match Resource Resource no invocation, object value doesn't match Resource Resource 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. --- build.gradle | 4 +- gradle.properties | 4 +- spring-data-rest-webmvc/build.gradle | 3 + .../webmvc/RepositoryRestHandlerAdapter.java | 2 +- ...cePostProcessorInvokingHandlerAdapter.java | 69 +++ ...essingHandlerMethodReturnValueHandler.java | 408 ++++++++++++++++++ ...dlerMethodReturnValueHandlerUnitTests.java | 335 ++++++++++++++ 7 files changed, 820 insertions(+), 5 deletions(-) create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcePostProcessorInvokingHandlerAdapter.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessingHandlerMethodReturnValueHandler.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ResourceProcessingHandlerMethodReturnValueHandlerUnitTests.java 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; + } + } +}