Skip to content

Commit

Permalink
SPR-8483 Add support for @RequestPart annotated method parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoyanchev committed Jun 28, 2011
1 parent 3bbefb3 commit 3a87d8e
Show file tree
Hide file tree
Showing 23 changed files with 912 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
import org.springframework.web.servlet.mvc.method.annotation.support.HttpEntityMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.support.ModelAndViewMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.support.PathVariableMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestPartMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.support.ServletCookieValueMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.ServletModelAttributeMethodProcessor;
Expand Down Expand Up @@ -352,6 +353,7 @@ private void initArgumentResolvers() {
argumentResolvers.addResolver(new PathVariableMethodArgumentResolver());
argumentResolvers.addResolver(new ServletModelAttributeMethodProcessor(false));
argumentResolvers.addResolver(new RequestResponseBodyMethodProcessor(messageConverters));
argumentResolvers.addResolver(new RequestPartMethodArgumentResolver(messageConverters));
argumentResolvers.addResolver(new RequestHeaderMethodArgumentResolver(beanFactory));
argumentResolvers.addResolver(new RequestHeaderMapMethodArgumentResolver());
argumentResolvers.addResolver(new ServletCookieValueMethodArgumentResolver(beanFactory));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright 2002-2011 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.method.annotation.support;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;

/**
* A base class for resolving method argument values by reading from the body of a request with {@link HttpMessageConverter}s.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*/
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {

protected final Log logger = LogFactory.getLog(getClass());

protected final List<HttpMessageConverter<?>> messageConverters;

protected final List<MediaType> allSupportedMediaTypes;

public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters);
}

/**
* Returns the media types supported by all provided message converters preserving their ordering and
* further sorting by specificity via {@link MediaType#sortBySpecificity(List)}.
*/
private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
}
List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes);
MediaType.sortBySpecificity(result);
return Collections.unmodifiableList(result);
}

/**
* Creates the method argument value of the expected parameter type by reading from the given request.
*
* @param <T> the expected type of the argument value to be created
* @param webRequest the current request
* @param methodParam the method argument
* @param paramType the type of the argument value to be created
* @return the created method argument value
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, Class<T> paramType) throws IOException,
HttpMediaTypeNotSupportedException {

HttpInputMessage inputMessage = createInputMessage(webRequest);
return readWithMessageConverters(inputMessage, methodParam, paramType);
}

/**
* Creates the method argument value of the expected parameter type by reading from the given HttpInputMessage.
*
* @param <T> the expected type of the argument value to be created
* @param inputMessage the HTTP input message representing the current request
* @param methodParam the method argument
* @param paramType the type of the argument value to be created
* @return the created method argument value
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, Class<T> paramType) throws IOException,
HttpMediaTypeNotSupportedException {

MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}

for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter.canRead(paramType, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
}
}

throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}

/**
* Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
*
* @param webRequest the web request to create an input message from
* @return the input message
*/
protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,99 +27,34 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerMapping;

/**
* A base class for resolving method argument values by reading from the body of a request with {@link
* HttpMessageConverter}s and for handling method return values by writing to the response with {@link
* HttpMessageConverter}s.
* Extends {@link AbstractMessageConverterMethodArgumentResolver} with the ability to handle method return
* values by writing to the response with {@link HttpMessageConverter}s.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*/
public abstract class AbstractMessageConverterMethodProcessor
implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {

private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");

protected final Log logger = LogFactory.getLog(getClass());

private final List<HttpMessageConverter<?>> messageConverters;

private final List<MediaType> allSupportedMediaTypes;

protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters);
}

/**
* Returns the media types supported by all provided message converters preserving their ordering and
* further sorting by specificity via {@link MediaType#sortBySpecificity(List)}.
*/
private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
}
List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes);
MediaType.sortBySpecificity(result);
return Collections.unmodifiableList(result);
}

@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest,
MethodParameter methodParam,
Class<T> paramType)
throws IOException, HttpMediaTypeNotSupportedException {

HttpInputMessage inputMessage = createInputMessage(webRequest);

MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}

for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter.canRead(paramType, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
}
}

throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}

/**
* Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
*
* @param webRequest the web request to create an input message from
* @return the input message
*/
protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
super(messageConverters);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2002-2011 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.method.annotation.support;

import java.lang.annotation.Annotation;
import java.util.List;

import javax.servlet.ServletRequest;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.multipart.RequestPartServletServerHttpRequest;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import org.springframework.web.util.WebUtils;

/**
* Resolves method arguments annotated with @{@link RequestPart} expecting the request to be a
* {@link MultipartHttpServletRequest} and binding the method argument to a specific part of the multipart request.
* The name of the part is derived either from the {@link RequestPart} annotation or from the name of the method
* argument as a fallback.
*
* <p>An @{@link RequestPart} method argument will be validated if annotated with {@code @Valid}. In case of
* validation failure, a {@link RequestPartNotValidException} is thrown and can be handled automatically through
* the {@link DefaultHandlerExceptionResolver}. A {@link Validator} can be configured globally in XML configuration
* with the Spring MVC namespace or in Java-based configuration with @{@link EnableWebMvc}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {

public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
}

public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestPart.class);
}

public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {

ServletRequest servletRequest = webRequest.getNativeRequest(ServletRequest.class);
MultipartHttpServletRequest multipartServletRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
if (multipartServletRequest == null) {
throw new IllegalStateException(
"Current request is not of type " + MultipartRequest.class.getName());
}

String partName = getPartName(parameter);
HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(multipartServletRequest, partName);

Object arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType());

if (isValidationApplicable(arg, parameter)) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, partName);
binder.validate();
Errors errors = binder.getBindingResult();
if (errors.hasErrors()) {
throw new RequestPartNotValidException(errors);
}
}

return arg;
}

private String getPartName(MethodParameter parameter) {
RequestPart annot = parameter.getParameterAnnotation(RequestPart.class);
String partName = annot.value();
if (partName.length() == 0) {
partName = parameter.getParameterName();
Assert.notNull(partName, "Request part name for argument type [" + parameter.getParameterType().getName()
+ "] not available, and parameter name information not found in class file either.");
}
return partName;
}

/**
* Whether to validate the given @{@link RequestPart} method argument. The default implementation checks
* if the parameter is also annotated with {@code @Valid}.
* @param argumentValue the validation candidate
* @param parameter the method argument declaring the validation candidate
* @return {@code true} if validation should be invoked, {@code false} otherwise.
*/
protected boolean isValidationApplicable(Object argumentValue, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if ("Valid".equals(annot.annotationType().getSimpleName())) {
return true;
}
}
return false;
}

}
Loading

0 comments on commit 3a87d8e

Please sign in to comment.