From ec9f94aac4a9c663ac4776cf6f0b43584a902660 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Tue, 19 Feb 2013 08:41:29 -0600 Subject: [PATCH] Added unit test for `ResourceProcessorHandlerMethodReturnValueHandler` as well as fix from pull request #61 --- ...tyResourceProcessorReturnValueHandler.java | 133 -------- ...cessorHandlerMethodReturnValueHandler.java | 2 +- ...sourceProcessorInvokingHandlerAdapter.java | 1 - .../data/rest/webmvc/HttpEntityMatcher.java | 50 +++ ...dlerMethodReturnValueHandlerUnitTests.java | 313 ++++++++++++++++++ .../rest/webmvc/ValidationErrors.properties | 2 + 6 files changed, 366 insertions(+), 135 deletions(-) delete mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceProcessorReturnValueHandler.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/HttpEntityMatcher.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ValidationErrors.properties diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceProcessorReturnValueHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceProcessorReturnValueHandler.java deleted file mode 100644 index 5ed58b421..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceProcessorReturnValueHandler.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import static org.springframework.data.util.ClassTypeInformation.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.springframework.core.MethodParameter; -import org.springframework.data.rest.webmvc.support.JsonpResponse; -import org.springframework.data.util.TypeInformation; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceProcessor; -import org.springframework.hateoas.Resources; -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 Jon Brisbin - */ -public class PersistentEntityResourceProcessorReturnValueHandler implements HandlerMethodReturnValueHandler { - - private final HandlerMethodReturnValueHandler delegate; - private final List processors = new ArrayList(); - - @SuppressWarnings({"unchecked"}) - public PersistentEntityResourceProcessorReturnValueHandler(HandlerMethodReturnValueHandler delegate, - List> processors) { - this.delegate = delegate; - for(ResourceProcessor rp : processors) { - TypeInformation componentType = from(rp.getClass()) - .getSuperTypeInformation(ResourceProcessor.class) - .getComponentType(); - if(Resources.class.isAssignableFrom(componentType.getType())) { - this.processors.add(new ResourcesProcessorWrapper(componentType.getComponentType().getType(), - (ResourceProcessor>)rp)); - } else if(Resource.class.isAssignableFrom(componentType.getType())) { - this.processors.add(new ResourceProcessorWrapper(componentType.getComponentType().getType(), - (ResourceProcessor>)rp)); - } - } - } - - @Override public boolean supportsReturnType(MethodParameter returnType) { - Class controller = returnType.getMethod().getDeclaringClass(); - return RepositoryController.class.isAssignableFrom(controller) - || RepositoryEntityController.class.isAssignableFrom(controller) - || RepositoryPropertyReferenceController.class.isAssignableFrom(controller) - || RepositorySearchController.class.isAssignableFrom(controller); - } - - @SuppressWarnings({"unchecked"}) - @Override - public void handleReturnValue(Object returnValue, - MethodParameter methodParam, - ModelAndViewContainer mavContainer, - NativeWebRequest nativeRequest) throws Exception { - Class returnValueType = returnValue.getClass(); - Class entityType = null; - - if(JsonpResponse.class.isAssignableFrom(returnValueType)) { - entityType = ((JsonpResponse)returnValue).getResponseEntity().getBody().getClass(); - } else if(ResponseEntity.class.isAssignableFrom(returnValueType)) { - entityType = ((ResponseEntity)returnValue).getBody().getClass(); - } else if(Resources.class.isAssignableFrom(returnValueType)) { - Collection c = ((Resources)returnValue).getContent(); - Object o; - if(null != c && !c.isEmpty() && null != (o = c.iterator().next())) { - entityType = o.getClass(); - } else { - if(delegate.supportsReturnType(methodParam)) { - delegate.handleReturnValue(returnValue, - methodParam, - mavContainer, - nativeRequest); - } - return; - } - } else if(Resource.class.isAssignableFrom(returnValueType)) { - entityType = ((Resource)returnValue).getContent().getClass(); - } - - for(Wrapper w : processors) { - if(w.type.isAssignableFrom(entityType)) { - if(ResourcesProcessorWrapper.class.isAssignableFrom(w.getClass()) - && Resources.class.isAssignableFrom(returnValueType)) { - ((ResourcesProcessorWrapper)w).processor.process((Resources)returnValue); - } else if(ResourceProcessorWrapper.class.isAssignableFrom(w.getClass()) - && Resource.class.isAssignableFrom(returnValueType)) { - ((ResourceProcessorWrapper)w).processor.process((Resource)returnValue); - } - } - } - - if(delegate.supportsReturnType(methodParam)) { - delegate.handleReturnValue(returnValue, - methodParam, - mavContainer, - nativeRequest); - } - } - - static class Wrapper { - Class type; - TypeInformation typeInfo; - - Wrapper(Class type) { - this.type = type; - this.typeInfo = from(type); - } - } - - static class ResourcesProcessorWrapper extends Wrapper { - ResourceProcessor> processor; - - ResourcesProcessorWrapper(Class type, ResourceProcessor> processor) { - super(type); - this.processor = processor; - } - } - - static class ResourceProcessorWrapper extends Wrapper { - ResourceProcessor> processor; - - ResourceProcessorWrapper(Class type, ResourceProcessor> processor) { - super(type); - this.processor = processor; - } - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java index 17946dd66..fad2184ba 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java @@ -347,7 +347,7 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler */ private static boolean isValueTypeMatch(Resource resource, TypeInformation target) { - if (resource == null || !target.getType().equals(resource.getClass())) { + if (resource == null || !target.getType().isAssignableFrom(resource.getClass())) { return false; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java index ad8ebaa50..b3e448b35 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java @@ -84,7 +84,6 @@ public class ResourceProcessorInvokingHandlerAdapter extends RequestMappingHandl // Set up ResourceProcessingHandlerMethodResolver to delegate to originally configured ones List newHandlers = new ArrayList(); - newHandlers.add(new PersistentEntityResourceProcessorReturnValueHandler(oldHandlers, resourcesProcessors)); newHandlers.add(new ResourceProcessorHandlerMethodReturnValueHandler(oldHandlers, resourcesProcessors)); // Configure the new handler to be used diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/HttpEntityMatcher.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/HttpEntityMatcher.java new file mode 100644 index 000000000..89cb22799 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/HttpEntityMatcher.java @@ -0,0 +1,50 @@ +package org.springframework.data.rest.webmvc; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; + +/** + * @author Jon Brisbin + */ +class HttpEntityMatcher extends BaseMatcher> { + + private final HttpEntity expected; + + public HttpEntityMatcher(HttpEntity expected) { + Assert.notNull(expected, "HttpEntity cannot be null"); + this.expected = expected; + } + + public static HttpEntityMatcher httpEntity(HttpEntity httpEntity) { + return new HttpEntityMatcher(httpEntity); + } + + @Override public boolean matches(Object item) { + if(!(item instanceof HttpEntity)) { + return false; + } + + if(item instanceof ResponseEntity && expected instanceof ResponseEntity) { + ResponseEntity left = (ResponseEntity)expected; + ResponseEntity right = (ResponseEntity)item; + + if(!left.getStatusCode().equals(right.getStatusCode())) { + return false; + } + } + + HttpEntity left = expected; + HttpEntity right = (HttpEntity)item; + + return left.getBody().equals(right.getBody()) + && left.getHeaders().equals(right.getHeaders()); + } + + @Override public void describeTo(Description description) { + description.appendText(expected.toString()); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java new file mode 100644 index 000000000..8b37b3441 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java @@ -0,0 +1,313 @@ +/* + * 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.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.rest.webmvc.HttpEntityMatcher.*; +import static org.springframework.util.ReflectionUtils.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hamcrest.Matcher; +import org.jmock.Expectations; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceProcessor; +import org.springframework.hateoas.Resources; +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; + +/** + * Unit tests for {@link org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler}. + * + * @author Oliver Gierke + * @author Jon Brisbin + */ +public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests extends AbstractJMockTests { + + static final Resource FOO = new Resource("foo"); + static final Resources> FOOS = new Resources>( + Collections.singletonList(FOO) + ); + static final StringResource FOO_RES = new StringResource("foo"); + static final HttpEntity> FOO_ENTITY = new HttpEntity>(FOO); + static final ResponseEntity> FOO_RESP_ENTITY = new ResponseEntity>( + FOO, + HttpStatus.OK + ); + static final HttpEntity FOO_RES_ENTITY = new HttpEntity(FOO_RES); + static final Resource BAR = new Resource("bar"); + static final Resources> BARS = new Resources>( + Collections.singletonList(BAR) + ); + static final StringResource BAR_RES = new StringResource("bar"); + static final HttpEntity> BAR_ENTITY = new HttpEntity>(BAR); + static final ResponseEntity> BAR_RESP_ENTITY = new ResponseEntity>( + BAR, + HttpStatus.OK + ); + static final HttpEntity BAR_RES_ENTITY = new HttpEntity(BAR_RES); + static final Resource LONG_10 = new Resource(10L); + static final Resource LONG_20 = new Resource(20L); + static final LongResource LONG_10_RES = new LongResource(10L); + static final LongResource LONG_20_RES = new LongResource(20L); + static final HttpEntity> LONG_10_ENTITY = new HttpEntity>(LONG_10); + static final HttpEntity LONG_10_RES_ENTITY = new HttpEntity(LONG_10_RES); + static final HttpEntity> LONG_20_ENTITY = new HttpEntity>(LONG_20); + static final HttpEntity LONG_20_RES_ENTITY = new HttpEntity(LONG_20_RES); + static final Map METHOD_PARAMS = new HashMap(); + + static { + doWithMethods(Controller.class, new MethodCallback() { + @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + METHOD_PARAMS.put(method.getName(), new MethodParameter(method, -1)); + } + }); + } + + HandlerMethodReturnValueHandler delegate; + List> resourceProcessors; + + @Before + public void setUp() { + delegate = context.mock(HandlerMethodReturnValueHandler.class); + resourceProcessors = new ArrayList>(); + } + + @Test + public void supportsIfDelegateSupports() { + assertSupport(true); + } + + @Test + public void doesNotSupportIfDelegateDoesNot() { + assertSupport(false); + } + + @Test + public void postProcessesStringResource() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("stringResourceEntity", is(BAR), FOO); + } + + @Test + public void postProcessesStringResourceInResponseEntity() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("stringResourceEntity", httpEntity(BAR_RESP_ENTITY), FOO_RESP_ENTITY); + } + + @Test + public void postProcessesStringResourceInWildcardResponseEntity() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("resourceEntity", httpEntity(BAR_RESP_ENTITY), FOO_RESP_ENTITY); + } + + @Test + public void postProcessesStringResources() throws Exception { + resourceProcessors.add(StringResourcesProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("resources", is(BARS), FOOS); + } + + @Test + public void postProcessesSpecializedStringResource() throws Exception { + resourceProcessors.add(SpecializedStringResourceProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("stringResourceEntity", httpEntity(BAR_RES_ENTITY), FOO_RES_ENTITY); + } + + @Test + public void postProcessesSpecializedStringUsingStringResourceProcessor() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("specializedStringResourceEntity", httpEntity(BAR_ENTITY), FOO_RES_ENTITY); + } + + @Test + public void postProcessesLongResource() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("longResource", is(LONG_20), LONG_10); + } + + @Test + public void postProcessesSpecializedLongResource() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(SpecializedLongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("specializedLongResourceEntity", httpEntity(LONG_20_RES_ENTITY), LONG_10_RES_ENTITY); + } + + @Test + public void doesNotPostProcesseLongResourceWithSpecializedLongResourceProcessor() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(SpecializedLongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("numberResourceEntity", httpEntity(LONG_10_ENTITY), LONG_10_ENTITY); + } + + @Test + public void postProcessesSpecializedLongResourceUsingLongResourceProcessor() throws Exception { + resourceProcessors.add(StringResourceProcessor.INSTANCE); + resourceProcessors.add(LongResourceProcessor.INSTANCE); + + invokeReturnValueHandler("resourceEntity", is(LONG_20), LONG_10_RES); + } + + // Helpers ---------------------------------------------------------// + private void invokeReturnValueHandler(String method, + final Matcher matcher, + Object returnValue) throws Exception { + final MethodParameter methodParam = METHOD_PARAMS.get(method); + + context.checking(new Expectations() {{ + oneOf(delegate).handleReturnValue(with(matcher), + with(methodParam), + with(aNull(ModelAndViewContainer.class)), + with(aNull(NativeWebRequest.class))); + }}); + + HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler( + delegate, + resourceProcessors + ); + handler.handleReturnValue(returnValue, methodParam, null, null); + } + + private void assertSupport(final boolean value) { + final MethodParameter parameter = context.mock(MethodParameter.class); + + context.checking(new Expectations() {{ + oneOf(delegate).supportsReturnType(parameter); + will(returnValue(value)); + }}); + + HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler( + delegate, + resourceProcessors + ); + + assertThat(handler.supportsReturnType(parameter), is(value)); + } + + enum StringResourceProcessor implements ResourceProcessor> { + INSTANCE; + + @Override public Resource process(Resource resource) { + return BAR; + } + } + + enum LongResourceProcessor implements ResourceProcessor> { + INSTANCE; + + @Override public Resource process(Resource resource) { + return LONG_20; + } + } + + enum StringResourcesProcessor implements ResourceProcessor>> { + INSTANCE; + + @Override public Resources> process(Resources> resource) { + return BARS; + } + } + + enum SpecializedStringResourceProcessor implements ResourceProcessor { + INSTANCE; + + @Override + public StringResource process(StringResource resource) { + return BAR_RES; + } + } + + enum SpecializedLongResourceProcessor implements ResourceProcessor { + INSTANCE; + + @Override + public LongResource process(LongResource resource) { + return LONG_20_RES; + } + } + + static interface Controller { + + Resources> resources(); + + Resource resource(); + + Resource longResource(); + + StringResource specializedResource(); + + Object object(); + + HttpEntity> resourceEntity(); + + HttpEntity> resourcesEntity(); + + HttpEntity objectEntity(); + + HttpEntity> stringResourceEntity(); + + HttpEntity> numberResourceEntity(); + + HttpEntity specializedStringResourceEntity(); + + HttpEntity specializedLongResourceEntity(); + + ResponseEntity> resourceResponseEntity(); + + ResponseEntity> resourcesResponseEntity(); + } + + static class StringResource extends Resource { + public StringResource(String value) { + super(value); + } + } + + static class LongResource extends Resource { + public LongResource(Long value) { + super(value); + } + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ValidationErrors.properties b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ValidationErrors.properties new file mode 100644 index 000000000..f9beeb27c --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ValidationErrors.properties @@ -0,0 +1,2 @@ +field.name.required = Field {0}.{1} is required. +no.userid = {0}s must be assigned initial userids. \ No newline at end of file