SPR-5409 - Support for PUTting and POSTing non-form data

This commit is contained in:
Arjen Poutsma 2009-03-25 16:33:27 +00:00
parent 93c56f19df
commit 035eea01e8
6 changed files with 252 additions and 79 deletions

View File

@ -60,6 +60,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.style.StylerUtils;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
@ -547,7 +548,7 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl
public PortletHandlerMethodInvoker(HandlerMethodResolver resolver) {
super(resolver, webBindingInitializer, sessionAttributeStore,
parameterNameDiscoverer, customArgumentResolvers);
parameterNameDiscoverer, customArgumentResolvers, new HttpMessageConverter[0]);
}
@Override

View File

@ -0,0 +1,23 @@
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;
/**
* Annotation which indicates that a method parameter should be bound to the web request body. Supported for annotated
* handler methods in Servlet environments.
*
* @author Arjen Poutsma
* @see RequestHeader
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
* @since 3.0
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {
}

View File

@ -19,9 +19,11 @@ package org.springframework.web.bind.annotation.support;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -35,6 +37,9 @@ import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.ClassUtils;
@ -42,11 +47,13 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.DefaultSessionAttributeStore;
@ -89,24 +96,28 @@ public class HandlerMethodInvoker {
private final SimpleSessionStatus sessionStatus = new SimpleSessionStatus();
private final HttpMessageConverter[] messageConverters;
public HandlerMethodInvoker(HandlerMethodResolver methodResolver) {
this(methodResolver, null);
}
public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer) {
this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null);
this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null, new WebArgumentResolver[0],
new HttpMessageConverter[0]);
}
public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer,
SessionAttributeStore sessionAttributeStore, ParameterNameDiscoverer parameterNameDiscoverer,
WebArgumentResolver... customArgumentResolvers) {
WebArgumentResolver[] customArgumentResolvers, HttpMessageConverter[] messageConverters) {
this.methodResolver = methodResolver;
this.bindingInitializer = bindingInitializer;
this.sessionAttributeStore = sessionAttributeStore;
this.parameterNameDiscoverer = parameterNameDiscoverer;
this.customArgumentResolvers = customArgumentResolvers;
this.messageConverters = messageConverters;
}
@ -159,6 +170,7 @@ public class HandlerMethodInvoker {
GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
String paramName = null;
String headerName = null;
boolean requestBodyFound = false;
String cookieName = null;
String pathVarName = null;
String attrName = null;
@ -182,6 +194,10 @@ public class HandlerMethodInvoker {
defaultValue = requestHeader.defaultValue();
found++;
}
else if (RequestBody.class.isInstance(paramAnn)) {
requestBodyFound = true;
found++;
}
else if (CookieValue.class.isInstance(paramAnn)) {
CookieValue cookieValue = (CookieValue) paramAnn;
cookieName = cookieValue.value();
@ -238,6 +254,9 @@ public class HandlerMethodInvoker {
else if (headerName != null) {
args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler);
}
else if (requestBodyFound) {
args[i] = resolveRequestBody(methodParam, webRequest, handler);
}
else if (cookieName != null) {
args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler);
}
@ -418,6 +437,45 @@ public class HandlerMethodInvoker {
return binder.convertIfNecessary(headerValue, paramType, methodParam);
}
/**
* Resolves the given {@link RequestBody @RequestBody} annotation.
* Throws an UnsupportedOperationException by default.
*/
@SuppressWarnings("unchecked")
protected Object resolveRequestBody(MethodParameter methodParam, NativeWebRequest webRequest, Object handler)
throws Exception {
HttpInputMessage inputMessage = createHttpInputMessage(webRequest);
Class paramType = methodParam.getParameterType();
MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) {
throw new IllegalStateException("Cannot extract response: no Content-Type found");
}
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
if (messageConverter.supports(paramType)) {
for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) {
if (supportedMediaType.includes(contentType)) {
return messageConverter.read(paramType, inputMessage);
}
}
}
}
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}
/**
* Returns a {@link HttpInputMessage} for the given {@link NativeWebRequest}.
* Throws an UnsupportedOperationException by default.
*/
protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
throw new UnsupportedOperationException("@RequestBody not supported");
}
private Object resolveCookieValue(String cookieName, boolean required, String defaultValue,
MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall)
throws Exception {

View File

@ -51,6 +51,14 @@ import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.AntPathMatcher;
@ -60,6 +68,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.HttpSessionRequiredException;
import org.springframework.web.bind.MissingServletRequestParameterException;
@ -149,6 +158,9 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
private final Map<Class<?>, ServletHandlerMethodResolver> methodResolverCache =
new ConcurrentHashMap<Class<?>, ServletHandlerMethodResolver>();
private HttpMessageConverter<?>[] messageConverters =
new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
new FormHttpMessageConverter(), new SourceHttpMessageConverter()};
public AnnotationMethodHandlerAdapter() {
// no restriction of HTTP methods by default
@ -291,6 +303,16 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
this.customArgumentResolvers = argumentResolvers;
}
/**
* Set the message body converters to use. These converters are used to convert
* from and to HTTP requests and responses.
*/
public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
}
public boolean supports(Object handler) {
return getMethodResolver(handler).hasHandlerMethods();
@ -346,13 +368,15 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
catch (HttpRequestMethodNotSupportedException ex) {
return handleHttpRequestMethodNotSupportedException(ex, request, response);
}
catch (HttpMediaTypeNotSupportedException ex) {
return handleHttpMediaTypeNotSupportedException(ex, request, response);
}
}
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
/**
* Handle the case where no request handler method was found.
* <p>The default implementation logs a warning and sends an HTTP 404 error.
@ -394,6 +418,27 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
return null;
}
/**
* Handle the case where no {@linkplain HttpMessageConverter message converters} was found for the PUT or POSTed
* content.
* <p>The default implementation logs a warning, sends an HTTP 415 error and sets the "Allow" header.
* Alternatively, a fallback view could be chosen, or the HttpMediaTypeNotSupportedException
* could be rethrown as-is.
* @param ex the HttpMediaTypeNotSupportedException to be handled
* @param request current HTTP request
* @param response current HTTP response
* @return a ModelAndView to render, or <code>null</code> if handled directly
* @throws Exception an Exception that should be thrown as result of the servlet request
*/
protected ModelAndView handleHttpMediaTypeNotSupportedException(
HttpMediaTypeNotSupportedException ex, HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
response.addHeader("Accept", MediaType.toString(ex.getSupportedMediaTypes()));
return null;
}
/**
* Template method for creating a new ServletRequestDataBinder instance.
* <p>The default implementation creates a standard ServletRequestDataBinder.
@ -593,7 +638,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
private ServletHandlerMethodInvoker(HandlerMethodResolver resolver) {
super(resolver, webBindingInitializer, sessionAttributeStore,
parameterNameDiscoverer, customArgumentResolvers);
parameterNameDiscoverer, customArgumentResolvers, messageConverters);
}
@Override
@ -625,6 +670,12 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
}
}
@Override
protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest();
return new ServletServerHttpRequest(servletRequest);
}
@Override
protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest)
throws Exception {

View File

@ -42,6 +42,7 @@ import org.junit.Test;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.aop.interceptor.SimpleTraceInterceptor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.BeansException;
import org.springframework.beans.DerivedTestBean;
import org.springframework.beans.ITestBean;
import org.springframework.beans.TestBean;
@ -66,6 +67,7 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@ -93,18 +95,11 @@ import org.springframework.web.util.NestedServletException;
*/
public class ServletAnnotationControllerTests {
private DispatcherServlet servlet;
@Test
public void standardHandleMethod() throws Exception {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(MyController.class));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
initServlet(MyController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do");
MockHttpServletResponse response = new MockHttpServletResponse();
@ -114,16 +109,7 @@ public class ServletAnnotationControllerTests {
@Test(expected = MissingServletRequestParameterException.class)
public void requiredParamMissing() throws Exception {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(RequiredParamController.class));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
initServlet(RequiredParamController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do");
MockHttpServletResponse response = new MockHttpServletResponse();
@ -132,16 +118,7 @@ public class ServletAnnotationControllerTests {
@Test
public void optionalParamPresent() throws Exception {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(OptionalParamController.class));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
initServlet(OptionalParamController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do");
request.addParameter("id", "val");
@ -154,16 +131,7 @@ public class ServletAnnotationControllerTests {
@Test
public void optionalParamMissing() throws Exception {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(OptionalParamController.class));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
initServlet(OptionalParamController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do");
MockHttpServletResponse response = new MockHttpServletResponse();
@ -173,16 +141,7 @@ public class ServletAnnotationControllerTests {
@Test
public void defaultParamMissing() throws Exception {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(DefaultValueParamController.class));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
initServlet(DefaultValueParamController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do");
MockHttpServletResponse response = new MockHttpServletResponse();
@ -192,16 +151,7 @@ public class ServletAnnotationControllerTests {
@Test
public void methodNotAllowed() throws Exception {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(MethodNotAllowedController.class));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
initServlet(MethodNotAllowedController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do");
MockHttpServletResponse response = new MockHttpServletResponse();
@ -285,17 +235,23 @@ public class ServletAnnotationControllerTests {
doTestAdaptedHandleMethods(MyAdaptedController3.class);
}
private void doTestAdaptedHandleMethods(final Class<?> controllerClass) throws Exception {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
private void initServlet(final Class<?> controllerclass) throws ServletException {
servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent)
throws BeansException {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(controllerClass));
wac.registerBeanDefinition("controller", new RootBeanDefinition(controllerclass));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
}
private void doTestAdaptedHandleMethods(final Class<?> controllerClass) throws Exception {
initServlet(controllerClass);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath1.do");
MockHttpServletResponse response = new MockHttpServletResponse();
@ -886,16 +842,7 @@ public class ServletAnnotationControllerTests {
@Test
public void pathOrdering() throws ServletException, IOException {
@SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() {
@Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", new RootBeanDefinition(PathOrderingController.class));
wac.refresh();
return wac;
}
};
servlet.init(new MockServletConfig());
initServlet(PathOrderingController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dir/myPath1.do");
MockHttpServletResponse response = new MockHttpServletResponse();
@ -903,6 +850,34 @@ public class ServletAnnotationControllerTests {
assertEquals("method1", response.getContentAsString());
}
@Test
public void requestBody() 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");
MockHttpServletResponse response = new MockHttpServletResponse();
servlet.service(request, response);
assertEquals(requestBody, response.getContentAsString());
}
@Test
public void unsupportedRequestBody() 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", "application/pdf");
MockHttpServletResponse response = new MockHttpServletResponse();
servlet.service(request, response);
assertEquals("Invalid response status code", HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
response.getStatus());
assertNotNull("No Accept response header set", response.getHeader("Accept"));
}
/*
* Controllers
*/
@ -1482,4 +1457,13 @@ public class ServletAnnotationControllerTests {
}
}
@Controller
public static class RequestBodyController {
@RequestMapping(value = "/something", method = RequestMethod.PUT)
public void handle(@RequestBody String body, Writer writer) throws IOException {
writer.write(body);
}
}
}

View File

@ -0,0 +1,56 @@
package org.springframework.web;
import java.util.List;
import javax.servlet.ServletException;
import org.springframework.http.MediaType;
/**
* Exception thrown when a client POSTs or PUTs content
* not supported by request handler does not support a
* specific request method.
*
* @author Arjen Poutsma
* @since 3.0
*/
public class HttpMediaTypeNotSupportedException extends ServletException {
private MediaType contentType;
private List<MediaType> supportedMediaTypes;
/**
* Create a new HttpMediaTypeNotSupportedException.
* @param contentType the unsupported content type
* @param supportedMediaTypes the list of supported media types
*/
public HttpMediaTypeNotSupportedException(MediaType contentType, List<MediaType> supportedMediaTypes) {
this(contentType, supportedMediaTypes, "Content type '" + contentType + "' not supported");
}
/**
* Create a new HttpMediaTypeNotSupportedException.
* @param contentType the unsupported content type
* @param supportedMediaTypes the list of supported media types
* @param msg the detail message
*/
public HttpMediaTypeNotSupportedException(MediaType contentType, List<MediaType> supportedMediaTypes, String msg) {
super(msg);
this.contentType = contentType;
this.supportedMediaTypes = supportedMediaTypes;
}
/**
* Return the HTTP request content type method that caused the failure.
*/
public MediaType getContentType() {
return contentType;
}
/**
* Return the list of supported media types.
*/
public List<MediaType> getSupportedMediaTypes() {
return supportedMediaTypes;
}
}