Added unit test for ResourceProcessorHandlerMethodReturnValueHandler as well as fix from pull request #61

This commit is contained in:
Jon Brisbin
2013-02-19 08:41:29 -06:00
committed by Jon Brisbin
parent 3b600f9ba8
commit ec9f94aac4
6 changed files with 366 additions and 135 deletions

View File

@@ -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<Wrapper> processors = new ArrayList<Wrapper>();
@SuppressWarnings({"unchecked"})
public PersistentEntityResourceProcessorReturnValueHandler(HandlerMethodReturnValueHandler delegate,
List<ResourceProcessor<?>> 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<Resources<?>>)rp));
} else if(Resource.class.isAssignableFrom(componentType.getType())) {
this.processors.add(new ResourceProcessorWrapper(componentType.getComponentType().getType(),
(ResourceProcessor<Resource<?>>)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<Resources<?>> processor;
ResourcesProcessorWrapper(Class<?> type, ResourceProcessor<Resources<?>> processor) {
super(type);
this.processor = processor;
}
}
static class ResourceProcessorWrapper extends Wrapper {
ResourceProcessor<Resource<?>> processor;
ResourceProcessorWrapper(Class<?> type, ResourceProcessor<Resource<?>> processor) {
super(type);
this.processor = processor;
}
}
}

View File

@@ -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;
}

View File

@@ -84,7 +84,6 @@ public class ResourceProcessorInvokingHandlerAdapter extends RequestMappingHandl
// Set up ResourceProcessingHandlerMethodResolver to delegate to originally configured ones
List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
newHandlers.add(new PersistentEntityResourceProcessorReturnValueHandler(oldHandlers, resourcesProcessors));
newHandlers.add(new ResourceProcessorHandlerMethodReturnValueHandler(oldHandlers, resourcesProcessors));
// Configure the new handler to be used

View File

@@ -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<T> extends BaseMatcher<HttpEntity<T>> {
private final HttpEntity<T> expected;
public HttpEntityMatcher(HttpEntity<T> expected) {
Assert.notNull(expected, "HttpEntity cannot be null");
this.expected = expected;
}
public static <T> HttpEntityMatcher<T> httpEntity(HttpEntity<T> httpEntity) {
return new HttpEntityMatcher<T>(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());
}
}

View File

@@ -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<String> FOO = new Resource<String>("foo");
static final Resources<Resource<String>> FOOS = new Resources<Resource<String>>(
Collections.singletonList(FOO)
);
static final StringResource FOO_RES = new StringResource("foo");
static final HttpEntity<Resource<String>> FOO_ENTITY = new HttpEntity<Resource<String>>(FOO);
static final ResponseEntity<Resource<String>> FOO_RESP_ENTITY = new ResponseEntity<Resource<String>>(
FOO,
HttpStatus.OK
);
static final HttpEntity<StringResource> FOO_RES_ENTITY = new HttpEntity<StringResource>(FOO_RES);
static final Resource<String> BAR = new Resource<String>("bar");
static final Resources<Resource<String>> BARS = new Resources<Resource<String>>(
Collections.singletonList(BAR)
);
static final StringResource BAR_RES = new StringResource("bar");
static final HttpEntity<Resource<String>> BAR_ENTITY = new HttpEntity<Resource<String>>(BAR);
static final ResponseEntity<Resource<String>> BAR_RESP_ENTITY = new ResponseEntity<Resource<String>>(
BAR,
HttpStatus.OK
);
static final HttpEntity<StringResource> BAR_RES_ENTITY = new HttpEntity<StringResource>(BAR_RES);
static final Resource<Long> LONG_10 = new Resource<Long>(10L);
static final Resource<Long> LONG_20 = new Resource<Long>(20L);
static final LongResource LONG_10_RES = new LongResource(10L);
static final LongResource LONG_20_RES = new LongResource(20L);
static final HttpEntity<Resource<Long>> LONG_10_ENTITY = new HttpEntity<Resource<Long>>(LONG_10);
static final HttpEntity<LongResource> LONG_10_RES_ENTITY = new HttpEntity<LongResource>(LONG_10_RES);
static final HttpEntity<Resource<Long>> LONG_20_ENTITY = new HttpEntity<Resource<Long>>(LONG_20);
static final HttpEntity<LongResource> LONG_20_RES_ENTITY = new HttpEntity<LongResource>(LONG_20_RES);
static final Map<String, MethodParameter> METHOD_PARAMS = new HashMap<String, MethodParameter>();
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<ResourceProcessor<?>> resourceProcessors;
@Before
public void setUp() {
delegate = context.mock(HandlerMethodReturnValueHandler.class);
resourceProcessors = new ArrayList<ResourceProcessor<?>>();
}
@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<Resource<String>> {
INSTANCE;
@Override public Resource<String> process(Resource<String> resource) {
return BAR;
}
}
enum LongResourceProcessor implements ResourceProcessor<Resource<Long>> {
INSTANCE;
@Override public Resource<Long> process(Resource<Long> resource) {
return LONG_20;
}
}
enum StringResourcesProcessor implements ResourceProcessor<Resources<Resource<String>>> {
INSTANCE;
@Override public Resources<Resource<String>> process(Resources<Resource<String>> resource) {
return BARS;
}
}
enum SpecializedStringResourceProcessor implements ResourceProcessor<StringResource> {
INSTANCE;
@Override
public StringResource process(StringResource resource) {
return BAR_RES;
}
}
enum SpecializedLongResourceProcessor implements ResourceProcessor<LongResource> {
INSTANCE;
@Override
public LongResource process(LongResource resource) {
return LONG_20_RES;
}
}
static interface Controller {
Resources<Resource<String>> resources();
Resource<String> resource();
Resource<Long> longResource();
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();
HttpEntity<LongResource> specializedLongResourceEntity();
ResponseEntity<Resource<?>> resourceResponseEntity();
ResponseEntity<Resources<?>> resourcesResponseEntity();
}
static class StringResource extends Resource<String> {
public StringResource(String value) {
super(value);
}
}
static class LongResource extends Resource<Long> {
public LongResource(Long value) {
super(value);
}
}
}

View File

@@ -0,0 +1,2 @@
field.name.required = Field {0}.{1} is required.
no.userid = {0}s must be assigned initial userids.