From b11970ed8d3a1ee051ba0cb22d57bce144bd650b Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 28 Jul 2009 13:12:12 +0000 Subject: [PATCH] SPR-5923 - HttpMessageConverter selection as a result of @ResponseBody should consider the requested content type --- .../AnnotationMethodHandlerAdapter.java | 39 +++++++++--- .../DefaultHandlerExceptionResolver.java | 30 +++++++++- .../ServletAnnotationControllerTests.java | 15 +++++ .../web/HttpMediaTypeException.java | 59 +++++++++++++++++++ .../HttpMediaTypeNotAcceptableException.java | 47 +++++++++++++++ .../HttpMediaTypeNotSupportedException.java | 16 +---- 6 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index f5d3e212bcf..bc25d62104e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; +import java.io.IOException; import java.lang.reflect.Method; import java.security.Principal; import java.util.ArrayList; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.ServletException; import javax.servlet.ServletRequest; @@ -51,6 +53,7 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; import org.springframework.http.converter.BufferedImageHttpMessageConverter; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.FormHttpMessageConverter; @@ -71,6 +74,7 @@ import org.springframework.util.StringUtils; import org.springframework.validation.support.BindingAwareModelMap; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.HttpSessionRequiredException; +import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.WebDataBinder; @@ -726,15 +730,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } if (returnValue != null && handlerMethod.isAnnotationPresent(ResponseBody.class)) { - Class returnValueType = returnValue.getClass(); - HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse()); - for (HttpMessageConverter messageConverter : messageConverters) { - if (messageConverter.supports(returnValueType)) { - messageConverter.write(returnValue, outputMessage); - responseArgumentUsed = true; - return null; - } - } + handleRequestBody(returnValue, webRequest); } if (returnValue instanceof ModelAndView) { @@ -777,6 +773,31 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen throw new IllegalArgumentException("Invalid handler method return value: " + returnValue); } } + + @SuppressWarnings("unchecked") + private void handleRequestBody(Object returnValue, ServletWebRequest webRequest) throws ServletException, IOException { + HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest()); + List acceptedMediaTypes = inputMessage.getHeaders().getAccept(); + HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse()); + Class returnValueType = returnValue.getClass(); + List allSupportedMediaTypes = new ArrayList(); + for (HttpMessageConverter messageConverter : messageConverters) { + allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); + if (messageConverter.supports(returnValueType)) { + for (Object o : messageConverter.getSupportedMediaTypes()) { + MediaType supportedMediaType = (MediaType) o; + for (MediaType acceptedMediaType : acceptedMediaTypes) { + if (supportedMediaType.includes(acceptedMediaType)) { + messageConverter.write(returnValue, outputMessage); + responseArgumentUsed = true; + return; + } + } + } + } + } + throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes); + } } static class RequestMappingInfo { diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index de0a56a50c2..b2c5f06d94b 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -33,6 +33,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; @@ -93,6 +94,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response, handler); } + else if (ex instanceof HttpMediaTypeNotAcceptableException) { + return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response, + handler); + } else if (ex instanceof MissingServletRequestParameterException) { return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request, response, handler); @@ -169,7 +174,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes /** * Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters} - * were found for the PUT or POSTed content.

The default implementation sends an HTTP 415 error, sets the "Allow" + * were found for the PUT or POSTed content.

The default implementation sends an HTTP 415 error, sets the "Accept" * header, and returns an empty {@code ModelAndView}. Alternatively, a fallback view could be chosen, or the * HttpMediaTypeNotSupportedException could be rethrown as-is. * @@ -194,6 +199,29 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes return new ModelAndView(); } + /** + * Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters} + * were found that were acceptable for the client (expressed via the {@code Accept} header. + *

The default implementation sends an HTTP 406 error and returns an empty {@code ModelAndView}. Alternatively, + * a fallback view could be chosen, or the HttpMediaTypeNotAcceptableException could be rethrown as-is. + * + * @param ex the HttpMediaTypeNotAcceptableException to be handled + * @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) + * @return a ModelAndView to render, or null if handled directly + * @throws Exception an Exception that should be thrown as result of the servlet request + */ + protected ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex, + HttpServletRequest request, + HttpServletResponse response, + Object handler) throws Exception { + + response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE); + return new ModelAndView(); + } + /** * Handle the case when a required parameter is missing.

The default implementation sends an HTTP 400 error, and * returns an empty {@code ModelAndView}. Alternatively, a fallback view could be chosen, or the diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index 140392ea500..c421f57ff99 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -876,11 +876,26 @@ public class ServletAnnotationControllerTests { String requestBody = "Hello World"; request.setContent(requestBody.getBytes("UTF-8")); request.addHeader("Content-Type", "text/plain; charset=utf-8"); + request.addHeader("Accept", "text/*"); MockHttpServletResponse response = new MockHttpServletResponse(); servlet.service(request, response); assertEquals(requestBody, response.getContentAsString()); } + @Test + public void responseBodyNoAcceptableMediaType() throws ServletException, IOException { + initServlet(RequestBodyController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); + String requestBody = "Hello World"; + request.setContent(requestBody.getBytes("UTF-8")); + request.addHeader("Content-Type", "text/plain; charset=utf-8"); + request.addHeader("Accept", "application/pdf, application/msword"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + assertEquals(406, response.getStatus()); + } + @Test public void unsupportedRequestBody() throws ServletException, IOException { initServlet(RequestBodyController.class); diff --git a/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeException.java b/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeException.java new file mode 100644 index 00000000000..add510f31fa --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeException.java @@ -0,0 +1,59 @@ +/* + * 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; + +import java.util.List; +import java.util.Collections; +import javax.servlet.ServletException; + +import org.springframework.http.MediaType; + +/** + * Abstract base for exceptions related to media types. Adds a list of supported {@link MediaType MediaTypes}. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public abstract class HttpMediaTypeException extends ServletException { + + private final List supportedMediaTypes; + + /** + * Create a new MediaTypeException. + * @param message the exception message + */ + protected HttpMediaTypeException(String message) { + super(message); + this.supportedMediaTypes = Collections.emptyList(); + } + + /** + * Create a new HttpMediaTypeNotSupportedException. + * @param supportedMediaTypes the list of supported media types + */ + protected HttpMediaTypeException(String message, List supportedMediaTypes) { + super(message); + this.supportedMediaTypes = supportedMediaTypes; + } + + /** + * Return the list of supported media types. + */ + public List getSupportedMediaTypes() { + return supportedMediaTypes; + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java b/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java new file mode 100644 index 00000000000..3d2a4362da3 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java @@ -0,0 +1,47 @@ +/* + * 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; + +import java.util.List; + +import org.springframework.http.MediaType; + +/** + * Exception thrown when the request handler cannot generate a response that is acceptable by the client. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException { + + /** + * Create a new HttpMediaTypeNotAcceptableException. + * @param message the exception message + */ + public HttpMediaTypeNotAcceptableException(String message) { + super(message); + } + + /** + * Create a new HttpMediaTypeNotSupportedException. + * @param supportedMediaTypes the list of supported media types + */ + public HttpMediaTypeNotAcceptableException(List supportedMediaTypes) { + super("Could not find acceptable representation", supportedMediaTypes); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java b/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java index 163826ec679..68fe27551cf 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java +++ b/org.springframework.web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java @@ -16,9 +16,7 @@ package org.springframework.web; -import java.util.Collections; import java.util.List; -import javax.servlet.ServletException; import org.springframework.http.MediaType; @@ -29,12 +27,10 @@ import org.springframework.http.MediaType; * @author Arjen Poutsma * @since 3.0 */ -public class HttpMediaTypeNotSupportedException extends ServletException { +public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException { private final MediaType contentType; - private final List supportedMediaTypes; - /** * Create a new HttpMediaTypeNotSupportedException. * @param message the exception message @@ -42,7 +38,6 @@ public class HttpMediaTypeNotSupportedException extends ServletException { public HttpMediaTypeNotSupportedException(String message) { super(message); this.contentType = null; - this.supportedMediaTypes = Collections.emptyList(); } /** @@ -61,9 +56,8 @@ public class HttpMediaTypeNotSupportedException extends ServletException { * @param msg the detail message */ public HttpMediaTypeNotSupportedException(MediaType contentType, List supportedMediaTypes, String msg) { - super(msg); + super(msg, supportedMediaTypes); this.contentType = contentType; - this.supportedMediaTypes = supportedMediaTypes; } /** @@ -73,10 +67,4 @@ public class HttpMediaTypeNotSupportedException extends ServletException { return contentType; } - /** - * Return the list of supported media types. - */ - public List getSupportedMediaTypes() { - return supportedMediaTypes; - } }