Consistent support for path variable and multipart binding

Closes gh-24107
Closes gh-22169
Closes gh-25265
This commit is contained in:
Juergen Hoeller
2020-08-06 18:28:09 +02:00
parent 5bdbbdfcfb
commit d61c0ee57d
13 changed files with 365 additions and 85 deletions

View File

@@ -48,7 +48,6 @@ import org.springframework.web.method.ControllerAdviceBean;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
import org.springframework.web.method.annotation.MapMethodProcessor;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.annotation.ModelMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
@@ -361,7 +360,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
@@ -375,7 +374,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
}
// Catch-all
handlers.add(new ModelAttributeMethodProcessor(true));
handlers.add(new ServletModelAttributeMethodProcessor(true));
return handlers;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 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.
@@ -31,6 +31,8 @@ import org.springframework.web.servlet.HandlerMapping;
*
* @author Rossen Stoyanchev
* @since 3.1
* @see ServletRequestDataBinder
* @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
*/
public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
@@ -60,9 +62,9 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
* Merge URI variables into the property values to use for data binding.
*/
@Override
@SuppressWarnings("unchecked")
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
@SuppressWarnings("unchecked")
Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr);
if (uriVars != null) {
uriVars.forEach((name, value) -> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 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.
@@ -58,7 +58,8 @@ public class ModelAndViewResolverMethodReturnValueHandler implements HandlerMeth
@Nullable
private final List<ModelAndViewResolver> mavResolvers;
private final ModelAttributeMethodProcessor modelAttributeProcessor = new ModelAttributeMethodProcessor(true);
private final ModelAttributeMethodProcessor modelAttributeProcessor =
new ServletModelAttributeMethodProcessor(true);
/**

View File

@@ -75,7 +75,6 @@ import org.springframework.web.method.annotation.ErrorsMethodArgumentResolver;
import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver;
import org.springframework.web.method.annotation.InitBinderDataBinderFactory;
import org.springframework.web.method.annotation.MapMethodProcessor;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.annotation.ModelFactory;
import org.springframework.web.method.annotation.ModelMethodProcessor;
import org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver;
@@ -739,7 +738,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
@@ -757,7 +756,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
}
else {
handlers.add(new ModelAttributeMethodProcessor(true));
handlers.add(new ServletModelAttributeMethodProcessor(true));
}
return handlers;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 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.
@@ -108,8 +108,8 @@ public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodPr
return null;
}
@SuppressWarnings("unchecked")
protected final Map<String, String> getUriTemplateVariables(NativeWebRequest request) {
@SuppressWarnings("unchecked")
Map<String, String> variables = (Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return (variables != null ? variables : Collections.emptyMap());
@@ -158,4 +158,23 @@ public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodPr
servletBinder.bind(servletRequest);
}
@Override
@Nullable
public Object resolveConstructorArgument(String paramName, Class<?> paramType, NativeWebRequest request)
throws Exception {
Object value = super.resolveConstructorArgument(paramName, paramType, request);
if (value != null) {
return value;
}
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
if (servletRequest != null) {
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
@SuppressWarnings("unchecked")
Map<String, String> uriVars = (Map<String, String>) servletRequest.getAttribute(attr);
return uriVars.get(paramName);
}
return null;
}
}

View File

@@ -57,6 +57,7 @@ import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Part;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlRootElement;
@@ -108,6 +109,7 @@ import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
@@ -139,6 +141,7 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.support.StringMultipartFileEditor;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.ModelAndView;
@@ -158,6 +161,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.testfixture.servlet.MockMultipartFile;
import org.springframework.web.testfixture.servlet.MockMultipartHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockPart;
import org.springframework.web.testfixture.servlet.MockServletConfig;
import org.springframework.web.testfixture.servlet.MockServletContext;
@@ -1934,6 +1938,44 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertThat(response.getContentAsString()).isEqualTo("value1-true-0");
}
@PathPatternsParameterizedTest
void dataClassBindingWithPathVariable(boolean usePathPatterns) throws Exception {
initDispatcherServlet(PathVariableDataClassController.class, usePathPatterns);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind/true");
request.addParameter("param1", "value1");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertThat(response.getContentAsString()).isEqualTo("value1-true-0");
}
@PathPatternsParameterizedTest
void dataClassBindingWithMultipartFile(boolean usePathPatterns) throws Exception {
initDispatcherServlet(MultipartFileDataClassController.class, usePathPatterns);
MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest();
request.setRequestURI("/bind");
request.addFile(new MockMultipartFile("param1", "value1".getBytes(StandardCharsets.UTF_8)));
request.addParameter("param2", "true");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertThat(response.getContentAsString()).isEqualTo("value1-true-0");
}
@PathPatternsParameterizedTest
void dataClassBindingWithServletPart(boolean usePathPatterns) throws Exception {
initDispatcherServlet(ServletPartDataClassController.class, usePathPatterns);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setContentType("multipart/form-data");
request.setRequestURI("/bind");
request.addPart(new MockPart("param1", "value1".getBytes(StandardCharsets.UTF_8)));
request.addParameter("param2", "true");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertThat(response.getContentAsString()).isEqualTo("value1-true-0");
}
@PathPatternsParameterizedTest
void dataClassBindingWithAdditionalSetter(boolean usePathPatterns) throws Exception {
initDispatcherServlet(DataClassController.class, usePathPatterns);
@@ -3831,6 +3873,15 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
}
@RestController
public static class PathVariableDataClassController {
@RequestMapping("/bind/{param2}")
public String handle(DataClass data) {
return data.param1 + "-" + data.param2 + "-" + data.param3;
}
}
@RestController
public static class ValidatedDataClassController {
@@ -3873,6 +3924,70 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
}
public static class MultipartFileDataClass {
@NotNull
public final MultipartFile param1;
public final boolean param2;
public int param3;
@ConstructorProperties({"param1", "param2", "optionalParam"})
public MultipartFileDataClass(MultipartFile param1, boolean p2, Optional<Integer> optionalParam) {
this.param1 = param1;
this.param2 = p2;
Assert.notNull(optionalParam, "Optional must not be null");
optionalParam.ifPresent(integer -> this.param3 = integer);
}
public void setParam3(int param3) {
this.param3 = param3;
}
}
@RestController
public static class MultipartFileDataClassController {
@RequestMapping("/bind")
public String handle(MultipartFileDataClass data) throws IOException {
return StreamUtils.copyToString(data.param1.getInputStream(), StandardCharsets.UTF_8) +
"-" + data.param2 + "-" + data.param3;
}
}
public static class ServletPartDataClass {
@NotNull
public final Part param1;
public final boolean param2;
public int param3;
@ConstructorProperties({"param1", "param2", "optionalParam"})
public ServletPartDataClass(Part param1, boolean p2, Optional<Integer> optionalParam) {
this.param1 = param1;
this.param2 = p2;
Assert.notNull(optionalParam, "Optional must not be null");
optionalParam.ifPresent(integer -> this.param3 = integer);
}
public void setParam3(int param3) {
this.param3 = param3;
}
}
@RestController
public static class ServletPartDataClassController {
@RequestMapping("/bind")
public String handle(ServletPartDataClass data) throws IOException {
return StreamUtils.copyToString(data.param1.getInputStream(), StandardCharsets.UTF_8) +
"-" + data.param2 + "-" + data.param3;
}
}
@RestController
public static class OptionalDataClassController {