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

@@ -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 {