Skip to content

Commit 035eea0

Browse files
committed
SPR-5409 - Support for PUTting and POSTing non-form data
1 parent 93c56f1 commit 035eea0

File tree

6 files changed

+252
-79
lines changed

6 files changed

+252
-79
lines changed

Diff for: org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.springframework.core.ParameterNameDiscoverer;
6161
import org.springframework.core.annotation.AnnotationUtils;
6262
import org.springframework.core.style.StylerUtils;
63+
import org.springframework.http.converter.HttpMessageConverter;
6364
import org.springframework.ui.ExtendedModelMap;
6465
import org.springframework.ui.Model;
6566
import org.springframework.util.Assert;
@@ -547,7 +548,7 @@ private class PortletHandlerMethodInvoker extends HandlerMethodInvoker {
547548

548549
public PortletHandlerMethodInvoker(HandlerMethodResolver resolver) {
549550
super(resolver, webBindingInitializer, sessionAttributeStore,
550-
parameterNameDiscoverer, customArgumentResolvers);
551+
parameterNameDiscoverer, customArgumentResolvers, new HttpMessageConverter[0]);
551552
}
552553

553554
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.springframework.web.bind.annotation;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
9+
/**
10+
* Annotation which indicates that a method parameter should be bound to the web request body. Supported for annotated
11+
* handler methods in Servlet environments.
12+
*
13+
* @author Arjen Poutsma
14+
* @see RequestHeader
15+
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
16+
* @since 3.0
17+
*/
18+
@Target(ElementType.PARAMETER)
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Documented
21+
public @interface RequestBody {
22+
23+
}

Diff for: org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java

+60-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.InvocationTargetException;
2121
import java.lang.reflect.Method;
22+
import java.util.ArrayList;
2223
import java.util.Arrays;
2324
import java.util.Collection;
2425
import java.util.HashSet;
26+
import java.util.List;
2527
import java.util.Map;
2628
import java.util.Set;
2729

@@ -35,18 +37,23 @@
3537
import org.springframework.core.MethodParameter;
3638
import org.springframework.core.ParameterNameDiscoverer;
3739
import org.springframework.core.annotation.AnnotationUtils;
40+
import org.springframework.http.HttpInputMessage;
41+
import org.springframework.http.MediaType;
42+
import org.springframework.http.converter.HttpMessageConverter;
3843
import org.springframework.ui.ExtendedModelMap;
3944
import org.springframework.ui.Model;
4045
import org.springframework.util.ClassUtils;
4146
import org.springframework.util.ReflectionUtils;
4247
import org.springframework.util.StringUtils;
4348
import org.springframework.validation.BindingResult;
4449
import org.springframework.validation.Errors;
50+
import org.springframework.web.HttpMediaTypeNotSupportedException;
4551
import org.springframework.web.bind.WebDataBinder;
4652
import org.springframework.web.bind.annotation.CookieValue;
4753
import org.springframework.web.bind.annotation.InitBinder;
4854
import org.springframework.web.bind.annotation.ModelAttribute;
4955
import org.springframework.web.bind.annotation.PathVariable;
56+
import org.springframework.web.bind.annotation.RequestBody;
5057
import org.springframework.web.bind.annotation.RequestHeader;
5158
import org.springframework.web.bind.annotation.RequestParam;
5259
import org.springframework.web.bind.support.DefaultSessionAttributeStore;
@@ -89,24 +96,28 @@ public class HandlerMethodInvoker {
8996

9097
private final SimpleSessionStatus sessionStatus = new SimpleSessionStatus();
9198

99+
private final HttpMessageConverter[] messageConverters;
100+
92101

93102
public HandlerMethodInvoker(HandlerMethodResolver methodResolver) {
94103
this(methodResolver, null);
95104
}
96105

97106
public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer) {
98-
this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null);
107+
this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null, new WebArgumentResolver[0],
108+
new HttpMessageConverter[0]);
99109
}
100110

101111
public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer,
102112
SessionAttributeStore sessionAttributeStore, ParameterNameDiscoverer parameterNameDiscoverer,
103-
WebArgumentResolver... customArgumentResolvers) {
113+
WebArgumentResolver[] customArgumentResolvers, HttpMessageConverter[] messageConverters) {
104114

105115
this.methodResolver = methodResolver;
106116
this.bindingInitializer = bindingInitializer;
107117
this.sessionAttributeStore = sessionAttributeStore;
108118
this.parameterNameDiscoverer = parameterNameDiscoverer;
109119
this.customArgumentResolvers = customArgumentResolvers;
120+
this.messageConverters = messageConverters;
110121
}
111122

112123

@@ -159,6 +170,7 @@ private Object[] resolveHandlerArguments(Method handlerMethod,
159170
GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
160171
String paramName = null;
161172
String headerName = null;
173+
boolean requestBodyFound = false;
162174
String cookieName = null;
163175
String pathVarName = null;
164176
String attrName = null;
@@ -182,6 +194,10 @@ else if (RequestHeader.class.isInstance(paramAnn)) {
182194
defaultValue = requestHeader.defaultValue();
183195
found++;
184196
}
197+
else if (RequestBody.class.isInstance(paramAnn)) {
198+
requestBodyFound = true;
199+
found++;
200+
}
185201
else if (CookieValue.class.isInstance(paramAnn)) {
186202
CookieValue cookieValue = (CookieValue) paramAnn;
187203
cookieName = cookieValue.value();
@@ -238,6 +254,9 @@ else if (BeanUtils.isSimpleProperty(paramType)) {
238254
else if (headerName != null) {
239255
args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler);
240256
}
257+
else if (requestBodyFound) {
258+
args[i] = resolveRequestBody(methodParam, webRequest, handler);
259+
}
241260
else if (cookieName != null) {
242261
args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler);
243262
}
@@ -418,6 +437,45 @@ else if (required) {
418437
return binder.convertIfNecessary(headerValue, paramType, methodParam);
419438
}
420439

440+
/**
441+
* Resolves the given {@link RequestBody @RequestBody} annotation.
442+
* Throws an UnsupportedOperationException by default.
443+
*/
444+
@SuppressWarnings("unchecked")
445+
protected Object resolveRequestBody(MethodParameter methodParam, NativeWebRequest webRequest, Object handler)
446+
throws Exception {
447+
448+
HttpInputMessage inputMessage = createHttpInputMessage(webRequest);
449+
450+
Class paramType = methodParam.getParameterType();
451+
MediaType contentType = inputMessage.getHeaders().getContentType();
452+
if (contentType == null) {
453+
throw new IllegalStateException("Cannot extract response: no Content-Type found");
454+
}
455+
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
456+
for (HttpMessageConverter<?> messageConverter : messageConverters) {
457+
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
458+
if (messageConverter.supports(paramType)) {
459+
for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) {
460+
if (supportedMediaType.includes(contentType)) {
461+
return messageConverter.read(paramType, inputMessage);
462+
}
463+
}
464+
}
465+
}
466+
467+
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
468+
}
469+
470+
/**
471+
* Returns a {@link HttpInputMessage} for the given {@link NativeWebRequest}.
472+
* Throws an UnsupportedOperationException by default.
473+
*/
474+
protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
475+
476+
throw new UnsupportedOperationException("@RequestBody not supported");
477+
}
478+
421479
private Object resolveCookieValue(String cookieName, boolean required, String defaultValue,
422480
MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall)
423481
throws Exception {

Diff for: org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java

+53-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@
5151
import org.springframework.core.MethodParameter;
5252
import org.springframework.core.ParameterNameDiscoverer;
5353
import org.springframework.core.annotation.AnnotationUtils;
54+
import org.springframework.http.HttpInputMessage;
55+
import org.springframework.http.MediaType;
56+
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
57+
import org.springframework.http.converter.FormHttpMessageConverter;
58+
import org.springframework.http.converter.HttpMessageConverter;
59+
import org.springframework.http.converter.StringHttpMessageConverter;
60+
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
61+
import org.springframework.http.server.ServletServerHttpRequest;
5462
import org.springframework.ui.ExtendedModelMap;
5563
import org.springframework.ui.Model;
5664
import org.springframework.util.AntPathMatcher;
@@ -60,6 +68,7 @@
6068
import org.springframework.util.PathMatcher;
6169
import org.springframework.util.StringUtils;
6270
import org.springframework.validation.support.BindingAwareModelMap;
71+
import org.springframework.web.HttpMediaTypeNotSupportedException;
6372
import org.springframework.web.HttpRequestMethodNotSupportedException;
6473
import org.springframework.web.HttpSessionRequiredException;
6574
import org.springframework.web.bind.MissingServletRequestParameterException;
@@ -149,6 +158,9 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
149158
private final Map<Class<?>, ServletHandlerMethodResolver> methodResolverCache =
150159
new ConcurrentHashMap<Class<?>, ServletHandlerMethodResolver>();
151160

161+
private HttpMessageConverter<?>[] messageConverters =
162+
new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
163+
new FormHttpMessageConverter(), new SourceHttpMessageConverter()};
152164

153165
public AnnotationMethodHandlerAdapter() {
154166
// no restriction of HTTP methods by default
@@ -291,6 +303,16 @@ public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers)
291303
this.customArgumentResolvers = argumentResolvers;
292304
}
293305

306+
/**
307+
* Set the message body converters to use. These converters are used to convert
308+
* from and to HTTP requests and responses.
309+
*/
310+
public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
311+
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
312+
this.messageConverters = messageConverters;
313+
}
314+
315+
294316

295317
public boolean supports(Object handler) {
296318
return getMethodResolver(handler).hasHandlerMethods();
@@ -346,13 +368,15 @@ protected ModelAndView invokeHandlerMethod(
346368
catch (HttpRequestMethodNotSupportedException ex) {
347369
return handleHttpRequestMethodNotSupportedException(ex, request, response);
348370
}
371+
catch (HttpMediaTypeNotSupportedException ex) {
372+
return handleHttpMediaTypeNotSupportedException(ex, request, response);
373+
}
349374
}
350375

351376
public long getLastModified(HttpServletRequest request, Object handler) {
352377
return -1;
353378
}
354379

355-
356380
/**
357381
* Handle the case where no request handler method was found.
358382
* <p>The default implementation logs a warning and sends an HTTP 404 error.
@@ -394,6 +418,27 @@ protected ModelAndView handleHttpRequestMethodNotSupportedException(
394418
return null;
395419
}
396420

421+
/**
422+
* Handle the case where no {@linkplain HttpMessageConverter message converters} was found for the PUT or POSTed
423+
* content.
424+
* <p>The default implementation logs a warning, sends an HTTP 415 error and sets the "Allow" header.
425+
* Alternatively, a fallback view could be chosen, or the HttpMediaTypeNotSupportedException
426+
* could be rethrown as-is.
427+
* @param ex the HttpMediaTypeNotSupportedException to be handled
428+
* @param request current HTTP request
429+
* @param response current HTTP response
430+
* @return a ModelAndView to render, or <code>null</code> if handled directly
431+
* @throws Exception an Exception that should be thrown as result of the servlet request
432+
*/
433+
protected ModelAndView handleHttpMediaTypeNotSupportedException(
434+
HttpMediaTypeNotSupportedException ex, HttpServletRequest request, HttpServletResponse response)
435+
throws Exception {
436+
437+
response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
438+
response.addHeader("Accept", MediaType.toString(ex.getSupportedMediaTypes()));
439+
return null;
440+
}
441+
397442
/**
398443
* Template method for creating a new ServletRequestDataBinder instance.
399444
* <p>The default implementation creates a standard ServletRequestDataBinder.
@@ -593,7 +638,7 @@ private class ServletHandlerMethodInvoker extends HandlerMethodInvoker {
593638

594639
private ServletHandlerMethodInvoker(HandlerMethodResolver resolver) {
595640
super(resolver, webBindingInitializer, sessionAttributeStore,
596-
parameterNameDiscoverer, customArgumentResolvers);
641+
parameterNameDiscoverer, customArgumentResolvers, messageConverters);
597642
}
598643

599644
@Override
@@ -625,6 +670,12 @@ protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean
625670
}
626671
}
627672

673+
@Override
674+
protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
675+
HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest();
676+
return new ServletServerHttpRequest(servletRequest);
677+
}
678+
628679
@Override
629680
protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest)
630681
throws Exception {

0 commit comments

Comments
 (0)