Support attribute overrides with @ResponseStatus

This commit introduces support for attribute overrides for
@ResponseStatus when @ResponseStatus is used as a meta-annotation on
a custom composed annotation.

Specifically, this commit migrates all code that looks up
@ResponseStatus from using AnnotationUtils.findAnnotation() to using
AnnotatedElementUtils.findMergedAnnotation().

Issue: SPR-13441
This commit is contained in:
Sam Brannen 2015-09-11 20:31:44 +02:00
parent 4a49ce9694
commit e2bfbdcfd1
8 changed files with 164 additions and 65 deletions

View File

@ -16,8 +16,12 @@
package org.springframework.test.web.servlet.samples.standalone.resultmatchers; package org.springframework.test.web.servlet.samples.standalone.resultmatchers;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@ -26,6 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
@ -34,6 +39,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
* Examples of expectations on the status and the status reason found in the response. * Examples of expectations on the status and the status reason found in the response.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen
*/ */
public class StatusAssertionTests { public class StatusAssertionTests {
@ -42,12 +48,14 @@ public class StatusAssertionTests {
@Test @Test
public void testStatusInt() throws Exception { public void testStatusInt() throws Exception {
this.mockMvc.perform(get("/created")).andExpect(status().is(201)); this.mockMvc.perform(get("/created")).andExpect(status().is(201));
this.mockMvc.perform(get("/createdWithComposedAnnotation")).andExpect(status().is(201));
this.mockMvc.perform(get("/badRequest")).andExpect(status().is(400)); this.mockMvc.perform(get("/badRequest")).andExpect(status().is(400));
} }
@Test @Test
public void testHttpStatus() throws Exception { public void testHttpStatus() throws Exception {
this.mockMvc.perform(get("/created")).andExpect(status().isCreated()); this.mockMvc.perform(get("/created")).andExpect(status().isCreated());
this.mockMvc.perform(get("/createdWithComposedAnnotation")).andExpect(status().isCreated());
this.mockMvc.perform(get("/badRequest")).andExpect(status().isBadRequest()); this.mockMvc.perform(get("/badRequest")).andExpect(status().isBadRequest());
} }
@ -66,27 +74,43 @@ public class StatusAssertionTests {
@Test @Test
public void testReasonMatcher() throws Exception { public void testReasonMatcher() throws Exception {
this.mockMvc.perform(get("/badRequest")) this.mockMvc.perform(get("/badRequest")).andExpect(status().reason(endsWith("token")));
.andExpect(status().reason(endsWith("token")));
} }
@RequestMapping
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface Get {
@AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] path() default {};
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus status() default INTERNAL_SERVER_ERROR;
}
@Controller @Controller
private static class StatusController { private static class StatusController {
@RequestMapping("/created") @RequestMapping("/created")
@ResponseStatus(HttpStatus.CREATED) @ResponseStatus(CREATED)
public @ResponseBody void created(){ public @ResponseBody void created(){
} }
@Get(path = "/createdWithComposedAnnotation", status = CREATED)
public @ResponseBody void createdWithComposedAnnotation() {
}
@RequestMapping("/badRequest") @RequestMapping("/badRequest")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Expired token") @ResponseStatus(code = BAD_REQUEST, reason = "Expired token")
public @ResponseBody void badRequest(){ public @ResponseBody void badRequest(){
} }
@RequestMapping("/notImplemented") @RequestMapping("/notImplemented")
@ResponseStatus(HttpStatus.NOT_IMPLEMENTED) @ResponseStatus(NOT_IMPLEMENTED)
public @ResponseBody void notImplemented(){ public @ResponseBody void notImplemented(){
} }
} }
} }

View File

@ -25,7 +25,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.BridgeMethodResolver; import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -33,15 +33,17 @@ import org.springframework.util.ClassUtils;
/** /**
* Encapsulates information about a handler method consisting of a * Encapsulates information about a handler method consisting of a
* {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}. * {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
* Provides convenient access to method parameters, method return value, method annotations. * Provides convenient access to method parameters, the method return value,
* method annotations, etc.
* *
* <p>The class may be created with a bean instance or with a bean name (e.g. lazy-init bean, * <p>The class may be created with a bean instance or with a bean name (e.g. lazy-init bean,
* prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@link HandlerMethod} * prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@code HandlerMethod}
* instance with a bean instance resolved through the associated {@link BeanFactory}. * instance with a bean instance resolved through the associated {@link BeanFactory}.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 3.1 * @since 3.1
*/ */
public class HandlerMethod { public class HandlerMethod {
@ -98,7 +100,7 @@ public class HandlerMethod {
/** /**
* Create an instance from a bean name, a method, and a {@code BeanFactory}. * Create an instance from a bean name, a method, and a {@code BeanFactory}.
* The method {@link #createWithResolvedBean()} may be used later to * The method {@link #createWithResolvedBean()} may be used later to
* re-create the {@code HandlerMethod} with an initialized the bean. * re-create the {@code HandlerMethod} with an initialized bean.
*/ */
public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) { public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
Assert.hasText(beanName, "Bean name is required"); Assert.hasText(beanName, "Bean name is required");
@ -222,11 +224,14 @@ public class HandlerMethod {
/** /**
* Returns a single annotation on the underlying method traversing its super methods * Returns a single annotation on the underlying method traversing its super methods
* if no annotation can be found on the given method itself. * if no annotation can be found on the given method itself.
* <p>Also supports <em>merged</em> composed annotations with attribute
* overrides as of Spring Framework 4.2.2.
* @param annotationType the type of annotation to introspect the method for. * @param annotationType the type of annotation to introspect the method for.
* @return the annotation, or {@code null} if none found * @return the annotation, or {@code null} if none found
* @see AnnotatedElementUtils#findMergedAnnotation
*/ */
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) { public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
return AnnotationUtils.findAnnotation(this.method, annotationType); return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
} }
/** /**

View File

@ -56,6 +56,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -119,7 +120,7 @@ import org.springframework.web.util.WebUtils;
/** /**
* Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface * Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface
* that maps handler methods based on HTTP paths, HTTP methods and request parameters * that maps handler methods based on HTTP paths, HTTP methods, and request parameters
* expressed through the {@link RequestMapping} annotation. * expressed through the {@link RequestMapping} annotation.
* *
* <p>Supports request parameter binding through the {@link RequestParam} annotation. * <p>Supports request parameter binding through the {@link RequestParam} annotation.
@ -133,6 +134,7 @@ import org.springframework.web.util.WebUtils;
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Sam Brannen
* @since 2.5 * @since 2.5
* @see #setPathMatcher * @see #setPathMatcher
* @see #setMethodNameResolver * @see #setMethodNameResolver
@ -911,19 +913,19 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator
public ModelAndView getModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue, public ModelAndView getModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue,
ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception { ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception {
ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class); ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
if (responseStatusAnn != null) { if (responseStatus != null) {
HttpStatus responseStatus = responseStatusAnn.code(); HttpStatus statusCode = responseStatus.code();
String reason = responseStatusAnn.reason(); String reason = responseStatus.reason();
if (!StringUtils.hasText(reason)) { if (!StringUtils.hasText(reason)) {
webRequest.getResponse().setStatus(responseStatus.value()); webRequest.getResponse().setStatus(statusCode.value());
} }
else { else {
webRequest.getResponse().sendError(responseStatus.value(), reason); webRequest.getResponse().sendError(statusCode.value(), reason);
} }
// to be picked up by the RedirectView // to be picked up by the RedirectView
webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, responseStatus); webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, statusCode);
this.responseArgumentUsed = true; this.responseArgumentUsed = true;
} }

View File

@ -43,6 +43,7 @@ import javax.xml.transform.Source;
import org.springframework.core.ExceptionDepthComparator; import org.springframework.core.ExceptionDepthComparator;
import org.springframework.core.GenericTypeResolver; import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpInputMessage;
@ -377,15 +378,15 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc
private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest) private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest)
throws Exception { throws Exception {
ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class); ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
if (responseStatusAnn != null) { if (responseStatus != null) {
HttpStatus responseStatus = responseStatusAnn.code(); HttpStatus statusCode = responseStatus.code();
String reason = responseStatusAnn.reason(); String reason = responseStatus.reason();
if (!StringUtils.hasText(reason)) { if (!StringUtils.hasText(reason)) {
webRequest.getResponse().setStatus(responseStatus.value()); webRequest.getResponse().setStatus(statusCode.value());
} }
else { else {
webRequest.getResponse().sendError(responseStatus.value(), reason); webRequest.getResponse().sendError(statusCode.value(), reason);
} }
} }

View File

@ -22,7 +22,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware; import org.springframework.context.MessageSourceAware;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
@ -38,11 +38,14 @@ import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
* and the MVC Java config and the MVC namespace. * and the MVC Java config and the MVC namespace.
* *
* <p>As of 4.2 this resolver also looks recursively for {@code @ResponseStatus} * <p>As of 4.2 this resolver also looks recursively for {@code @ResponseStatus}
* present on cause exceptions. * present on cause exceptions, and as of 4.2.2 this resolver supports
* attribute overrides for {@code @ResponseStatus} in custom composed annotations.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen
* @since 3.0 * @since 3.0
* @see AnnotatedElementUtils#findMergedAnnotation
*/ */
public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware { public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
@ -59,7 +62,7 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) { Object handler, Exception ex) {
ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) { if (responseStatus != null) {
try { try {
return resolveResponseStatus(responseStatus, request, response, handler, ex); return resolveResponseStatus(responseStatus, request, response, handler, ex);

View File

@ -20,19 +20,20 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.io.Writer; import java.io.Writer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.BindException; import java.net.BindException;
import java.net.SocketException; import java.net.SocketException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
@ -43,25 +44,18 @@ import static org.junit.Assert.*;
/** /**
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
*/ */
@Deprecated @Deprecated
public class AnnotationMethodHandlerExceptionResolverTests { public class AnnotationMethodHandlerExceptionResolverTests {
private AnnotationMethodHandlerExceptionResolver exceptionResolver; private final AnnotationMethodHandlerExceptionResolver exceptionResolver = new AnnotationMethodHandlerExceptionResolver();
private MockHttpServletRequest request; private final MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
private MockHttpServletResponse response; private final MockHttpServletResponse response = new MockHttpServletResponse();
@Before
public void setUp() {
exceptionResolver = new AnnotationMethodHandlerExceptionResolver();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
request.setMethod("GET");
}
@Test @Test
public void simpleWithIOException() { public void simpleWithIOException() {
IOException ex = new IOException(); IOException ex = new IOException();
@ -103,6 +97,16 @@ public class AnnotationMethodHandlerExceptionResolverTests {
assertEquals("Invalid status code returned", 406, response.getStatus()); assertEquals("Invalid status code returned", 406, response.getStatus());
} }
@Test
public void simpleWithNumberFormatExceptionAndComposedResponseStatusAnnotation() {
NumberFormatException ex = new NumberFormatException();
SimpleController controller = new SimpleController();
ModelAndView mav = exceptionResolver.resolveException(request, response, controller, ex);
assertNotNull("No ModelAndView returned", mav);
assertEquals("Invalid view name returned", "X:NumberFormatException", mav.getViewName());
assertEquals("Invalid status code returned", 400, response.getStatus());
}
@Test @Test
public void inherited() { public void inherited() {
IOException ex = new IOException(); IOException ex = new IOException();
@ -155,6 +159,13 @@ public class AnnotationMethodHandlerExceptionResolverTests {
assertNull(mav); assertNull(mav);
} }
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedResponseStatus {
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus responseStatus() default HttpStatus.INTERNAL_SERVER_ERROR;
}
@Controller @Controller
private static class SimpleController { private static class SimpleController {
@ -162,18 +173,24 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler(IOException.class) @ExceptionHandler(IOException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleIOException(IOException ex, HttpServletRequest request) { public String handleIOException(IOException ex, HttpServletRequest request) {
return "X:" + ClassUtils.getShortName(ex.getClass()); return "X:" + ex.getClass().getSimpleName();
} }
@ExceptionHandler(SocketException.class) @ExceptionHandler(SocketException.class)
@ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE, reason = "This is simply unacceptable!") @ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE, reason = "This is simply unacceptable!")
public String handleSocketException(Exception ex, HttpServletResponse response) { public String handleSocketException(Exception ex, HttpServletResponse response) {
return "Y:" + ClassUtils.getShortName(ex.getClass()); return "Y:" + ex.getClass().getSimpleName();
} }
@ExceptionHandler(IllegalArgumentException.class) @ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgumentException(Exception ex) { public String handleIllegalArgumentException(Exception ex) {
return ClassUtils.getShortName(ex.getClass()); return ex.getClass().getSimpleName();
}
@ExceptionHandler(NumberFormatException.class)
@ComposedResponseStatus(responseStatus = HttpStatus.BAD_REQUEST)
public String handleNumberFormatException(NumberFormatException ex) {
return "X:" + ex.getClass().getSimpleName();
} }
} }
@ -194,12 +211,12 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler({BindException.class, IllegalArgumentException.class}) @ExceptionHandler({BindException.class, IllegalArgumentException.class})
public String handle1(Exception ex, HttpServletRequest request, HttpServletResponse response) public String handle1(Exception ex, HttpServletRequest request, HttpServletResponse response)
throws IOException { throws IOException {
return ClassUtils.getShortName(ex.getClass()); return ex.getClass().getSimpleName();
} }
@ExceptionHandler @ExceptionHandler
public String handle2(IllegalArgumentException ex) { public String handle2(IllegalArgumentException ex) {
return ClassUtils.getShortName(ex.getClass()); return ex.getClass().getSimpleName();
} }
} }
@ -209,7 +226,7 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public void handle(Exception ex, Writer writer) throws IOException { public void handle(Exception ex, Writer writer) throws IOException {
writer.write(ClassUtils.getShortName(ex.getClass())); writer.write(ex.getClass().getSimpleName());
} }
} }
@ -220,7 +237,7 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
@ResponseBody @ResponseBody
public String handle(Exception ex) { public String handle(Exception ex) {
return ClassUtils.getShortName(ex.getClass()); return ex.getClass().getSimpleName();
} }
} }

View File

@ -16,16 +16,16 @@
package org.springframework.web.servlet.mvc.annotation; package org.springframework.web.servlet.mvc.annotation;
import static org.junit.Assert.*; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale; import java.util.Locale;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.StaticMessageSource; import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockHttpServletResponse;
@ -33,22 +33,21 @@ import org.springframework.tests.sample.beans.ITestBean;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
/** @author Arjen Poutsma */ import static org.junit.Assert.*;
/**
* Integration tests for {@link ResponseStatusExceptionResolver}.
*
* @author Arjen Poutsma
* @author Sam Brannen
*/
public class ResponseStatusExceptionResolverTests { public class ResponseStatusExceptionResolverTests {
private ResponseStatusExceptionResolver exceptionResolver; private final ResponseStatusExceptionResolver exceptionResolver = new ResponseStatusExceptionResolver();
private MockHttpServletRequest request; private final MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
private MockHttpServletResponse response; private final MockHttpServletResponse response = new MockHttpServletResponse();
@Before
public void setUp() {
exceptionResolver = new ResponseStatusExceptionResolver();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
request.setMethod("GET");
}
@Test @Test
public void statusCode() { public void statusCode() {
@ -60,6 +59,16 @@ public class ResponseStatusExceptionResolverTests {
assertTrue("Response has not been committed", response.isCommitted()); assertTrue("Response has not been committed", response.isCommitted());
} }
@Test
public void statusCodeFromComposedResponseStatus() {
StatusCodeFromComposedResponseStatusException ex = new StatusCodeFromComposedResponseStatusException();
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertNotNull("No ModelAndView returned", mav);
assertTrue("No Empty ModelAndView returned", mav.isEmpty());
assertEquals("Invalid status code", 400, response.getStatus());
assertTrue("Response has not been committed", response.isCommitted());
}
@Test @Test
public void statusCodeAndReason() { public void statusCodeAndReason() {
StatusCodeAndReasonException ex = new StatusCodeAndReasonException(); StatusCodeAndReasonException ex = new StatusCodeAndReasonException();
@ -109,6 +118,7 @@ public class ResponseStatusExceptionResolverTests {
assertEquals("Invalid status code", 410, response.getStatus()); assertEquals("Invalid status code", 410, response.getStatus());
} }
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
@SuppressWarnings("serial") @SuppressWarnings("serial")
private static class StatusCodeException extends Exception { private static class StatusCodeException extends Exception {
@ -124,4 +134,17 @@ public class ResponseStatusExceptionResolverTests {
private static class StatusCodeAndReasonMessageException extends Exception { private static class StatusCodeAndReasonMessageException extends Exception {
} }
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedResponseStatus {
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus responseStatus() default HttpStatus.INTERNAL_SERVER_ERROR;
}
@ComposedResponseStatus(responseStatus = HttpStatus.BAD_REQUEST)
@SuppressWarnings("serial")
private static class StatusCodeFromComposedResponseStatusException extends Exception {
}
} }

View File

@ -16,9 +16,8 @@
package org.springframework.web.servlet.mvc.method.annotation; package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.*; import java.lang.annotation.Retention;
import static org.mockito.Mockito.*; import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -29,6 +28,7 @@ import javax.servlet.http.HttpServletResponse;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
@ -49,6 +49,9 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandlerCom
import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.servlet.view.RedirectView;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/** /**
* Test fixture with {@link ServletInvocableHandlerMethod}. * Test fixture with {@link ServletInvocableHandlerMethod}.
* *
@ -80,6 +83,16 @@ public class ServletInvocableHandlerMethodTests {
assertEquals(HttpStatus.BAD_REQUEST.value(), this.response.getStatus()); assertEquals(HttpStatus.BAD_REQUEST.value(), this.response.getStatus());
} }
@Test
public void invokeAndHandle_VoidWithComposedResponseStatus() throws Exception {
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "composedResponseStatus");
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
assertTrue("Null return value + @ComposedResponseStatus should result in 'request handled'",
this.mavContainer.isRequestHandled());
assertEquals(HttpStatus.BAD_REQUEST.value(), this.response.getStatus());
}
@Test @Test
public void invokeAndHandle_VoidWithHttpServletResponseArgument() throws Exception { public void invokeAndHandle_VoidWithHttpServletResponseArgument() throws Exception {
this.argumentResolvers.addResolver(new ServletResponseMethodArgumentResolver()); this.argumentResolvers.addResolver(new ServletResponseMethodArgumentResolver());
@ -260,6 +273,13 @@ public class ServletInvocableHandlerMethodTests {
return handlerMethod; return handlerMethod;
} }
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedResponseStatus {
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus responseStatus() default HttpStatus.INTERNAL_SERVER_ERROR;
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static class Handler { private static class Handler {
@ -277,6 +297,10 @@ public class ServletInvocableHandlerMethodTests {
return "foo"; return "foo";
} }
@ComposedResponseStatus(responseStatus = HttpStatus.BAD_REQUEST)
public void composedResponseStatus() {
}
public void httpServletResponse(HttpServletResponse response) { public void httpServletResponse(HttpServletResponse response) {
} }