diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java new file mode 100644 index 00000000000..ea241ff198f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2009 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.http.HttpStatus; + +/** + * Marks an exception class with the status code and reason that should be returned whenever said exception is thrown. + * + * @author Arjen Poutsma + * @since 3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ResponseStatus { + + /** + * The status code to use for the response. + * + * @see javax.servlet.http.HttpServletResponse#setStatus(int) + */ + HttpStatus value(); + + /** + * The reason to be used for the response.

If this element is not set, it will default to the standard status + * message for the status code. + * + * @see javax.servlet.http.HttpServletResponse#sendError(int, String) + */ + String reason() default ""; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties index c550bd71275..634ec163e3a 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties @@ -13,7 +13,8 @@ org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.m org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter -org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.handler.DefaultHandlerExceptionResolver +org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.handler.DefaultHandlerExceptionResolver,\ + org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java new file mode 100644 index 00000000000..f2852a0da7a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2009 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.web.servlet.mvc.annotation; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; + +/** + * Implementation of the {@link org.springframework.web.servlet.HandlerExceptionResolver HandlerExceptionResolver} + * interface that uses the {@link ResponseStatus @ResponseStatus} annotation to map exceptions to HTTP status codes. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver { + + @Override + protected ModelAndView doResolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); + if (responseStatus != null) { + try { + return resolveResponseStatus(responseStatus, request, response, handler, ex); + } + catch (Exception resolveEx) { + logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx); + } + } + return null; + } + + /** + * Template method that handles {@link ResponseStatus @ResponseStatus} annotation.

Default implementation send a + * response error using {@link HttpServletResponse#sendError(int)}, or {@link HttpServletResponse#sendError(int, + * String)} if the annotation has a {@linkplain ResponseStatus#reason() reason}. Returns an empty ModelAndView. + * + * @param responseStatus the annotation + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or null if none chosen at the time of the exception (for example, + * if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to, or null for default processing + */ + protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, + HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) throws Exception { + int statusCode = responseStatus.value().value(); + String reason = responseStatus.reason(); + if (!StringUtils.hasLength(reason)) { + response.sendError(statusCode); + } + else { + response.sendError(statusCode, reason); + } + return new ModelAndView(); + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java new file mode 100644 index 00000000000..d993d994e0c --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java @@ -0,0 +1,66 @@ +package org.springframework.web.servlet.mvc.annotation; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.ModelAndView; + +/** @author Arjen Poutsma */ +public class ResponseStatusExceptionResolverTests { + + private ResponseStatusExceptionResolver exceptionResolver; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @Before + public void setUp() { + exceptionResolver = new ResponseStatusExceptionResolver(); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + request.setMethod("GET"); + } + + @Test + public void statusCode() { + StatusCodeException ex = new StatusCodeException(); + 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()); + } + + @Test + public void statusCodeAndReason() { + StatusCodeAndReasonException ex = new StatusCodeAndReasonException(); + ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); + assertNotNull("No ModelAndView returned", mav); + assertTrue("No Empty ModelAndView returned", mav.isEmpty()); + assertEquals("Invalid status code", 410, response.getStatus()); + assertEquals("Invalid status reason", "You suck!", response.getErrorMessage()); + } + + @Test + public void notAnnotated() { + Exception ex = new Exception(); + exceptionResolver.resolveException(request, response, null, ex); + ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); + assertNull("ModelAndView returned", mav); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + private static class StatusCodeException extends Exception { + + } + + @ResponseStatus(value = HttpStatus.GONE, reason = "You suck!") + private static class StatusCodeAndReasonException extends Exception { + + } +}