Consistent support for path variable and multipart binding
Closes gh-24107 Closes gh-22169 Closes gh-25265
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) -> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user