Support constructing target object in DataBinder
See gh-26721
This commit is contained in:
parent
40bf923d7d
commit
ea398d7b7e
|
@ -17,30 +17,40 @@
|
|||
package org.springframework.validation;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeanInstantiationException;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.ConfigurablePropertyAccessor;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.PropertyAccessException;
|
||||
import org.springframework.beans.PropertyAccessorUtils;
|
||||
import org.springframework.beans.PropertyBatchUpdateException;
|
||||
import org.springframework.beans.PropertyEditorRegistrar;
|
||||
import org.springframework.beans.PropertyEditorRegistry;
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.SimpleTypeConverter;
|
||||
import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.format.Formatter;
|
||||
|
@ -50,10 +60,11 @@ import org.springframework.util.Assert;
|
|||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.annotation.ValidationAnnotationUtils;
|
||||
|
||||
/**
|
||||
* Binder that allows for setting property values on a target object, including
|
||||
* support for validation and binding result analysis.
|
||||
* Binder that allows applying property values to a target object via constructor
|
||||
* and setter injection, and also supports validation and binding result analysis.
|
||||
*
|
||||
* <p>The binding process can be customized by specifying allowed field patterns,
|
||||
* required fields, custom editors, etc.
|
||||
|
@ -105,6 +116,7 @@ import org.springframework.util.StringUtils;
|
|||
* @see #registerCustomEditor
|
||||
* @see #setMessageCodesResolver
|
||||
* @see #setBindingErrorProcessor
|
||||
* @see #construct
|
||||
* @see #bind
|
||||
* @see #getBindingResult
|
||||
* @see DefaultMessageCodesResolver
|
||||
|
@ -126,7 +138,10 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
protected static final Log logger = LogFactory.getLog(DataBinder.class);
|
||||
|
||||
@Nullable
|
||||
private final Object target;
|
||||
private Object target;
|
||||
|
||||
@Nullable
|
||||
ResolvableType targetType;
|
||||
|
||||
private final String objectName;
|
||||
|
||||
|
@ -136,7 +151,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
private boolean directFieldAccess = false;
|
||||
|
||||
@Nullable
|
||||
private SimpleTypeConverter typeConverter;
|
||||
private ExtendedTypeConverter typeConverter;
|
||||
|
||||
private boolean ignoreUnknownFields = true;
|
||||
|
||||
|
@ -193,6 +208,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
|
||||
/**
|
||||
* Return the wrapped target object.
|
||||
* <p>If the target object is {@code null} and {@link #getTargetType()} is set,
|
||||
* then {@link #construct(ValueResolver)} may be called to create the target.
|
||||
*/
|
||||
@Nullable
|
||||
public Object getTarget() {
|
||||
|
@ -206,6 +223,27 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
return this.objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type for the target object. When the target is {@code null},
|
||||
* setting the targetType allows using {@link #construct(ValueResolver)} to
|
||||
* create the target.
|
||||
* @param targetType the type of the target object
|
||||
* @since 6.1
|
||||
*/
|
||||
public void setTargetType(ResolvableType targetType) {
|
||||
Assert.state(this.target == null, "targetType is used to for target creation, but target is already set");
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link #setTargetType configured} type for the target object.
|
||||
* @since 6.1
|
||||
*/
|
||||
@Nullable
|
||||
public ResolvableType getTargetType() {
|
||||
return this.targetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this binder should attempt to "auto-grow" a nested path that contains a null value.
|
||||
* <p>If "true", a null path location will be populated with a default object value and traversed
|
||||
|
@ -213,6 +251,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
* when accessing an out-of-bounds index.
|
||||
* <p>Default is "true" on a standard DataBinder. Note that since Spring 4.1 this feature is supported
|
||||
* for bean property access (DataBinder's default mode) and field access.
|
||||
* <p>Used for setter/field injection via {@link #bind(PropertyValues)}, and not
|
||||
* applicable to constructor initialization via {@link #construct(ValueResolver)}.
|
||||
* @see #initBeanPropertyAccess()
|
||||
* @see org.springframework.beans.BeanWrapper#setAutoGrowNestedPaths
|
||||
*/
|
||||
|
@ -233,6 +273,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
* Specify the limit for array and collection auto-growing.
|
||||
* <p>Default is 256, preventing OutOfMemoryErrors in case of large indexes.
|
||||
* Raise this limit if your auto-growing needs are unusually high.
|
||||
* <p>Used for setter/field injection via {@link #bind(PropertyValues)}, and not
|
||||
* applicable to constructor initialization via {@link #construct(ValueResolver)}.
|
||||
* @see #initBeanPropertyAccess()
|
||||
* @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit
|
||||
*/
|
||||
|
@ -335,7 +377,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
*/
|
||||
protected SimpleTypeConverter getSimpleTypeConverter() {
|
||||
if (this.typeConverter == null) {
|
||||
this.typeConverter = new SimpleTypeConverter();
|
||||
this.typeConverter = new ExtendedTypeConverter();
|
||||
if (this.conversionService != null) {
|
||||
this.typeConverter.setConversionService(this.conversionService);
|
||||
}
|
||||
|
@ -389,6 +431,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
* <p>Note that this setting only applies to <i>binding</i> operations
|
||||
* on this DataBinder, not to <i>retrieving</i> values via its
|
||||
* {@link #getBindingResult() BindingResult}.
|
||||
* <p>Used for setter/field inject via {@link #bind(PropertyValues)}, and not
|
||||
* applicable to constructor initialization via {@link #construct(ValueResolver)},
|
||||
* which uses only the values it needs.
|
||||
* @see #bind
|
||||
*/
|
||||
public void setIgnoreUnknownFields(boolean ignoreUnknownFields) {
|
||||
|
@ -411,6 +456,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
* <p>Note that this setting only applies to <i>binding</i> operations
|
||||
* on this DataBinder, not to <i>retrieving</i> values via its
|
||||
* {@link #getBindingResult() BindingResult}.
|
||||
* <p>Used for setter/field inject via {@link #bind(PropertyValues)}, and not
|
||||
* applicable to constructor initialization via {@link #construct(ValueResolver)},
|
||||
* which uses only the values it needs.
|
||||
* @see #bind
|
||||
*/
|
||||
public void setIgnoreInvalidFields(boolean ignoreInvalidFields) {
|
||||
|
@ -439,6 +487,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
* <p>More sophisticated matching can be implemented by overriding the
|
||||
* {@link #isAllowed} method.
|
||||
* <p>Alternatively, specify a list of <i>disallowed</i> field patterns.
|
||||
* <p>Used for setter/field inject via {@link #bind(PropertyValues)}, and not
|
||||
* applicable to constructor initialization via {@link #construct(ValueResolver)},
|
||||
* which uses only the values it needs.
|
||||
* @param allowedFields array of allowed field patterns
|
||||
* @see #setDisallowedFields
|
||||
* @see #isAllowed(String)
|
||||
|
@ -475,6 +526,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
* <p>More sophisticated matching can be implemented by overriding the
|
||||
* {@link #isAllowed} method.
|
||||
* <p>Alternatively, specify a list of <i>allowed</i> field patterns.
|
||||
* <p>Used for setter/field inject via {@link #bind(PropertyValues)}, and not
|
||||
* applicable to constructor initialization via {@link #construct(ValueResolver)},
|
||||
* which uses only the values it needs.
|
||||
* @param disallowedFields array of disallowed field patterns
|
||||
* @see #setAllowedFields
|
||||
* @see #isAllowed(String)
|
||||
|
@ -508,6 +562,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
* incoming property values, a corresponding "missing field" error
|
||||
* will be created, with error code "required" (by the default
|
||||
* binding error processor).
|
||||
* <p>Used for setter/field inject via {@link #bind(PropertyValues)}, and not
|
||||
* applicable to constructor initialization via {@link #construct(ValueResolver)},
|
||||
* which uses only the values it needs.
|
||||
* @param requiredFields array of field names
|
||||
* @see #setBindingErrorProcessor
|
||||
* @see DefaultBindingErrorProcessor#MISSING_FIELD_ERROR_CODE
|
||||
|
@ -770,6 +827,133 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create the target with constructor injection of values. It is expected that
|
||||
* {@link #setTargetType(ResolvableType)} was previously called and that
|
||||
* {@link #getTarget()} is {@code null}.
|
||||
* <p>Uses a public, no-arg constructor if available in the target object type,
|
||||
* also supporting a "primary constructor" approach for data classes as follows:
|
||||
* It understands the JavaBeans {@code ConstructorProperties} annotation as
|
||||
* well as runtime-retained parameter names in the bytecode, associating
|
||||
* input values with constructor arguments by name. If no such constructor is
|
||||
* found, the default constructor will be used (even if not public), assuming
|
||||
* subsequent bean property bindings through setter methods.
|
||||
* <p>After the call, use {@link #getBindingResult()} to check for failures
|
||||
* to bind to, and/or validate constructor arguments. If there are no errors,
|
||||
* the target is set, and {@link #doBind(MutablePropertyValues)} can be used
|
||||
* for further initialization via setters.
|
||||
* @param valueResolver to resolve constructor argument values with
|
||||
* @throws BeanInstantiationException in case of constructor failure
|
||||
* @since 6.1
|
||||
*/
|
||||
public final void construct(ValueResolver valueResolver) {
|
||||
Assert.state(this.target == null, "Target instance already available");
|
||||
Assert.state(this.targetType != null, "Target type not set");
|
||||
|
||||
Class<?> clazz = this.targetType.resolve();
|
||||
clazz = (Optional.class.equals(clazz) ? this.targetType.resolveGeneric(0) : clazz);
|
||||
Assert.state(clazz != null, "Unknown data binding target type");
|
||||
|
||||
Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);
|
||||
if (ctor.getParameterCount() == 0) {
|
||||
// A single default constructor -> clearly a standard JavaBeans arrangement.
|
||||
this.target = BeanUtils.instantiateClass(ctor);
|
||||
}
|
||||
else {
|
||||
// A single data class constructor -> resolve constructor arguments from request parameters.
|
||||
String[] paramNames = BeanUtils.getParameterNames(ctor);
|
||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
||||
Object[] args = new Object[paramTypes.length];
|
||||
Set<String> failedParamNames = new HashSet<>(4);
|
||||
boolean bindFailure = false;
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
String paramName = paramNames[i];
|
||||
Class<?> paramType = paramTypes[i];
|
||||
Object value = valueResolver.resolveValue(paramName, paramType);
|
||||
try {
|
||||
MethodParameter methodParam = MethodParameter.forFieldAwareConstructor(ctor, i, paramName);
|
||||
if (value == null && methodParam.isOptional()) {
|
||||
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
|
||||
}
|
||||
else {
|
||||
args[i] = convertIfNecessary(value, paramType, methodParam);
|
||||
}
|
||||
}
|
||||
catch (TypeMismatchException ex) {
|
||||
ex.initPropertyName(paramName);
|
||||
args[i] = null;
|
||||
failedParamNames.add(paramName);
|
||||
getBindingResult().recordFieldValue(paramName, paramType, value);
|
||||
getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult());
|
||||
bindFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bindFailure) {
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
String paramName = paramNames[i];
|
||||
if (!failedParamNames.contains(paramName)) {
|
||||
Object value = args[i];
|
||||
getBindingResult().recordFieldValue(paramName, paramTypes[i], value);
|
||||
validateArgument(ctor.getDeclaringClass(), paramName, value);
|
||||
}
|
||||
}
|
||||
if (!(this.targetType.getSource() instanceof MethodParameter param && param.isOptional())) {
|
||||
try {
|
||||
this.target = BeanUtils.instantiateClass(ctor, args);
|
||||
}
|
||||
catch (BeanInstantiationException ex) {
|
||||
// swallow and proceed without target instance
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.target = BeanUtils.instantiateClass(ctor, args);
|
||||
}
|
||||
catch (BeanInstantiationException ex) {
|
||||
if (KotlinDetector.isKotlinType(clazz) && ex.getCause() instanceof NullPointerException cause) {
|
||||
ObjectError error = new ObjectError(ctor.getName(), cause.getMessage());
|
||||
getBindingResult().addError(error);
|
||||
return;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
// Now that target is set, add PropertyEditor's to PropertyAccessor
|
||||
if (this.typeConverter != null) {
|
||||
this.typeConverter.registerCustomEditors(getPropertyAccessor());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateArgument(Class<?> constructorClass, String name, @Nullable Object value) {
|
||||
Object[] validationHints = null;
|
||||
if (this.targetType.getSource() instanceof MethodParameter parameter) {
|
||||
for (Annotation ann : parameter.getParameterAnnotations()) {
|
||||
validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
|
||||
if (validationHints != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (validationHints == null) {
|
||||
return;
|
||||
}
|
||||
for (Validator validator : getValidatorsToApply()) {
|
||||
if (validator instanceof SmartValidator smartValidator) {
|
||||
try {
|
||||
smartValidator.validateValue(
|
||||
constructorClass, name, value, getBindingResult(), validationHints);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// No corresponding field on the target class...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the given property values to this binder's target.
|
||||
* <p>This call can create field errors, representing basic binding
|
||||
|
@ -972,4 +1156,36 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
return getBindingResult().getModel();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract to resolve a value in {@link #construct(ValueResolver)}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ValueResolver {
|
||||
|
||||
/**
|
||||
* Look up the value for a constructor argument.
|
||||
* @param name the argument name
|
||||
* @param type the argument type
|
||||
* @return the resolved value, possibly {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
Object resolveValue(String name, Class<?> type);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link SimpleTypeConverter} that is also {@link PropertyEditorRegistrar}.
|
||||
*/
|
||||
private static class ExtendedTypeConverter
|
||||
extends SimpleTypeConverter implements PropertyEditorRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerCustomEditors(PropertyEditorRegistry registry) {
|
||||
copyCustomEditorsTo(registry, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -16,15 +16,21 @@
|
|||
|
||||
package org.springframework.web.bind;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.Part;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartRequest;
|
||||
import org.springframework.web.multipart.support.StandardServletPartUtils;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
@ -46,10 +52,9 @@ import org.springframework.web.util.WebUtils;
|
|||
* which include specifying allowed/required fields, and registering custom
|
||||
* property editors.
|
||||
*
|
||||
* <p>Can also be used for manual data binding in custom web controllers:
|
||||
* for example, in a plain Controller implementation or in a MultiActionController
|
||||
* handler method. Simply instantiate a ServletRequestDataBinder for each binding
|
||||
* process, and invoke {@code bind} with the current ServletRequest as argument:
|
||||
* <p>Can also be used for manual data binding. Simply instantiate a
|
||||
* ServletRequestDataBinder for each binding process, and invoke {@code bind}
|
||||
* with the current ServletRequest as argument:
|
||||
*
|
||||
* <pre class="code">
|
||||
* MyBean myBean = new MyBean();
|
||||
|
@ -94,6 +99,27 @@ public class ServletRequestDataBinder extends WebDataBinder {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use a default or single data constructor to create the target by
|
||||
* binding request parameters, multipart files, or parts to constructor args.
|
||||
* <p>After the call, use {@link #getBindingResult()} to check for bind errors.
|
||||
* If there are none, the target is set, and {@link #bind(ServletRequest)}
|
||||
* can be called for further initialization via setters.
|
||||
* @param request the request to bind
|
||||
* @since 6.1
|
||||
*/
|
||||
public void construct(ServletRequest request) {
|
||||
construct(createValueResolver(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow subclasses to create the {@link ValueResolver} instance to use.
|
||||
* @since 6.1
|
||||
*/
|
||||
protected ServletRequestValueResolver createValueResolver(ServletRequest request) {
|
||||
return new ServletRequestValueResolver(request, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the parameters of the given request to this binder's target,
|
||||
* also binding multipart files in case of a multipart request.
|
||||
|
@ -119,7 +145,7 @@ public class ServletRequestDataBinder extends WebDataBinder {
|
|||
if (multipartRequest != null) {
|
||||
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
|
||||
}
|
||||
else if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MULTIPART_FORM_DATA_VALUE)) {
|
||||
else if (isFormDataPost(request)) {
|
||||
HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);
|
||||
if (httpServletRequest != null && HttpMethod.POST.matches(httpServletRequest.getMethod())) {
|
||||
StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles());
|
||||
|
@ -129,6 +155,10 @@ public class ServletRequestDataBinder extends WebDataBinder {
|
|||
doBind(mpvs);
|
||||
}
|
||||
|
||||
private static boolean isFormDataPost(ServletRequest request) {
|
||||
return StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MULTIPART_FORM_DATA_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point that subclasses can use to add extra bind values for a
|
||||
* request. Invoked before {@link #doBind(MutablePropertyValues)}.
|
||||
|
@ -153,4 +183,73 @@ public class ServletRequestDataBinder extends WebDataBinder {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code ServletRequest} {@link ValueResolver}. Mainly for use from
|
||||
* {@link org.springframework.web.bind.support.WebRequestDataBinder}.
|
||||
* @since 6.1
|
||||
*/
|
||||
public static ValueResolver valueResolver(ServletRequest request, WebDataBinder binder) {
|
||||
return new ServletRequestValueResolver(request, binder);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolver that looks up values to bind in a {@link ServletRequest}.
|
||||
*/
|
||||
protected static class ServletRequestValueResolver implements ValueResolver {
|
||||
|
||||
private final ServletRequest request;
|
||||
|
||||
private final WebDataBinder dataBinder;
|
||||
|
||||
protected ServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) {
|
||||
this.request = request;
|
||||
this.dataBinder = dataBinder;
|
||||
}
|
||||
|
||||
protected ServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public final Object resolveValue(String name, Class<?> paramType) {
|
||||
Object value = getRequestParameter(name, paramType);
|
||||
if (value == null) {
|
||||
value = this.dataBinder.resolvePrefixValue(name, paramType, this::getRequestParameter);
|
||||
}
|
||||
if (value == null) {
|
||||
value = getMultipartValue(name);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Object getRequestParameter(String name, Class<?> type) {
|
||||
Object value = this.request.getParameterValues(name);
|
||||
return (ObjectUtils.isArray(value) && Array.getLength(value) == 1 ? Array.get(value, 0) : value);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getMultipartValue(String name) {
|
||||
MultipartRequest multipartRequest = WebUtils.getNativeRequest(this.request, MultipartRequest.class);
|
||||
if (multipartRequest != null) {
|
||||
List<MultipartFile> files = multipartRequest.getFiles(name);
|
||||
if (!files.isEmpty()) {
|
||||
return (files.size() == 1 ? files.get(0) : files);
|
||||
}
|
||||
}
|
||||
else if (isFormDataPost(this.request)) {
|
||||
HttpServletRequest httpRequest = WebUtils.getNativeRequest(this.request, HttpServletRequest.class);
|
||||
if (httpRequest != null && HttpMethod.POST.matches(httpRequest.getMethod())) {
|
||||
List<Part> parts = StandardServletPartUtils.getParts(httpRequest, name);
|
||||
if (!parts.isEmpty()) {
|
||||
return (parts.size() == 1 ? parts.get(0) : parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -20,6 +20,7 @@ import java.lang.reflect.Array;
|
|||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.PropertyValue;
|
||||
|
@ -193,6 +194,33 @@ public class WebDataBinder extends DataBinder {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a value can be resolved if {@link #getFieldDefaultPrefix()}
|
||||
* or {@link #getFieldMarkerPrefix()} is prepended.
|
||||
* @param name the name of the value to resolve
|
||||
* @param type the type of value expected
|
||||
* @param resolver delegate resolver to use for the checks
|
||||
* @return the resolved value, or {@code null}
|
||||
* @since 6.1
|
||||
*/
|
||||
@Nullable
|
||||
protected Object resolvePrefixValue(String name, Class<?> type, BiFunction<String, Class<?>, Object> resolver) {
|
||||
Object value = resolver.apply(name, type);
|
||||
if (value == null) {
|
||||
String prefix = getFieldDefaultPrefix();
|
||||
if (prefix != null) {
|
||||
value = resolver.apply(prefix + name, type);
|
||||
}
|
||||
if (value == null) {
|
||||
prefix = getFieldMarkerPrefix();
|
||||
if (prefix != null && resolver.apply(prefix + name, type) != null) {
|
||||
value = getEmptyValue(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation performs a field default and marker check
|
||||
* before delegating to the superclass binding process.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -203,9 +203,11 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer
|
|||
if (this.bindingErrorProcessor != null) {
|
||||
binder.setBindingErrorProcessor(this.bindingErrorProcessor);
|
||||
}
|
||||
if (this.validator != null && binder.getTarget() != null &&
|
||||
this.validator.supports(binder.getTarget().getClass())) {
|
||||
binder.setValidator(this.validator);
|
||||
if (this.validator != null) {
|
||||
Class<?> type = getTargetType(binder);
|
||||
if (type != null && this.validator.supports(type)) {
|
||||
binder.setValidator(this.validator);
|
||||
}
|
||||
}
|
||||
if (this.conversionService != null) {
|
||||
binder.setConversionService(this.conversionService);
|
||||
|
@ -217,4 +219,16 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer
|
|||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Class<?> getTargetType(WebDataBinder binder) {
|
||||
Class<?> type = null;
|
||||
if (binder.getTarget() != null) {
|
||||
type = binder.getTarget().getClass();
|
||||
}
|
||||
else if (binder.getTargetType() != null) {
|
||||
type = binder.getTargetType().resolve();
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.springframework.web.bind.support;
|
|||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.validation.DataBinder;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
|
@ -69,11 +70,41 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
|
|||
public final WebDataBinder createBinder(
|
||||
NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
|
||||
|
||||
return createBinderInternal(webRequest, target, objectName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}.
|
||||
* <p>By default, if the parameter has {@code @Valid}, Bean Validation is
|
||||
* excluded, deferring to method validation.
|
||||
*/
|
||||
@Override
|
||||
public final WebDataBinder createBinder(
|
||||
NativeWebRequest webRequest, @Nullable Object target, String objectName,
|
||||
MethodParameter parameter) throws Exception {
|
||||
|
||||
return createBinderInternal(webRequest, target, objectName, parameter);
|
||||
}
|
||||
|
||||
private WebDataBinder createBinderInternal(
|
||||
NativeWebRequest webRequest, @Nullable Object target, String objectName,
|
||||
@Nullable MethodParameter parameter) throws Exception {
|
||||
|
||||
WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
|
||||
|
||||
if (target == null && parameter != null) {
|
||||
dataBinder.setTargetType(ResolvableType.forMethodParameter(parameter));
|
||||
}
|
||||
|
||||
if (this.initializer != null) {
|
||||
this.initializer.initBinder(dataBinder);
|
||||
}
|
||||
initBinder(dataBinder, webRequest);
|
||||
|
||||
if (this.methodValidationApplicable && parameter != null) {
|
||||
MethodValidationInitializer.initBinder(dataBinder, parameter);
|
||||
}
|
||||
|
||||
return dataBinder;
|
||||
}
|
||||
|
||||
|
@ -104,30 +135,13 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}.
|
||||
* <p>By default, if the parameter has {@code @Valid}, Bean Validation is
|
||||
* excluded, deferring to method validation.
|
||||
*/
|
||||
@Override
|
||||
public WebDataBinder createBinder(
|
||||
NativeWebRequest webRequest, @Nullable Object target, String objectName,
|
||||
MethodParameter parameter) throws Exception {
|
||||
|
||||
WebDataBinder dataBinder = createBinder(webRequest, target, objectName);
|
||||
if (this.methodValidationApplicable) {
|
||||
MethodValidationInitializer.updateBinder(dataBinder, parameter);
|
||||
}
|
||||
return dataBinder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Excludes Bean Validation if the method parameter has {@code @Valid}.
|
||||
*/
|
||||
private static class MethodValidationInitializer {
|
||||
|
||||
public static void updateBinder(DataBinder binder, MethodParameter parameter) {
|
||||
public static void initBinder(DataBinder binder, MethodParameter parameter) {
|
||||
for (Annotation annotation : parameter.getParameterAnnotations()) {
|
||||
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
|
||||
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.web.bind.support;
|
||||
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
|
@ -25,6 +26,7 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.ServletRequestDataBinder;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
@ -98,6 +100,25 @@ public class WebRequestDataBinder extends WebDataBinder {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use a default or single data constructor to create the target by
|
||||
* binding request parameters, multipart files, or parts to constructor args.
|
||||
* <p>After the call, use {@link #getBindingResult()} to check for bind errors.
|
||||
* If there are none, the target is set, and {@link #bind(WebRequest)}
|
||||
* can be called for further initialization via setters.
|
||||
* @param request the request to bind
|
||||
* @since 6.1
|
||||
*/
|
||||
public void construct(WebRequest request) {
|
||||
if (request instanceof NativeWebRequest nativeRequest) {
|
||||
ServletRequest servletRequest = nativeRequest.getNativeRequest(ServletRequest.class);
|
||||
if (servletRequest != null) {
|
||||
construct(ServletRequestDataBinder.valueResolver(servletRequest, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bind the parameters of the given request to this binder's target,
|
||||
* also binding multipart files in case of a multipart request.
|
||||
|
|
|
@ -17,37 +17,22 @@
|
|||
package org.springframework.web.method.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.Part;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeanInstantiationException;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.validation.SmartValidator;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.annotation.ValidationAnnotationUtils;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
|
@ -58,9 +43,6 @@ 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.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartRequest;
|
||||
import org.springframework.web.multipart.support.StandardServletPartUtils;
|
||||
|
||||
/**
|
||||
* Resolve {@code @ModelAttribute} annotated method arguments and handle
|
||||
|
@ -134,45 +116,45 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
mavContainer.setBinding(name, ann.binding());
|
||||
}
|
||||
|
||||
Object attribute = null;
|
||||
Object attribute;
|
||||
BindingResult bindingResult = null;
|
||||
|
||||
if (mavContainer.containsAttribute(name)) {
|
||||
attribute = mavContainer.getModel().get(name);
|
||||
}
|
||||
if (attribute == null || ObjectUtils.unwrapOptional(attribute) == null) {
|
||||
bindingResult = binderFactory.createBinder(webRequest, null, name).getBindingResult();
|
||||
attribute = wrapAsOptionalIfNecessary(parameter, null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Create attribute instance
|
||||
try {
|
||||
// Mainly to allow subclasses alternative to create attribute
|
||||
attribute = createAttribute(name, parameter, binderFactory, webRequest);
|
||||
}
|
||||
catch (MethodArgumentNotValidException ex) {
|
||||
if (isBindExceptionRequired(parameter)) {
|
||||
// No BindingResult parameter -> fail with BindException
|
||||
throw ex;
|
||||
}
|
||||
// Otherwise, expose null/empty value and associated BindingResult
|
||||
if (parameter.getParameterType() == Optional.class) {
|
||||
attribute = Optional.empty();
|
||||
}
|
||||
else {
|
||||
attribute = ex.getTarget();
|
||||
}
|
||||
attribute = wrapAsOptionalIfNecessary(parameter, ex.getTarget());
|
||||
bindingResult = ex.getBindingResult();
|
||||
}
|
||||
}
|
||||
|
||||
// No BindingResult yet, proceed with binding and validation
|
||||
if (bindingResult == null) {
|
||||
// Bean property binding and validation;
|
||||
// skipped in case of binding failure on construction.
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, parameter);
|
||||
if (binder.getTarget() != null) {
|
||||
if (attribute == null) {
|
||||
constructAttribute(binder, webRequest);
|
||||
attribute = wrapAsOptionalIfNecessary(parameter, binder.getTarget());
|
||||
}
|
||||
if (!binder.getBindingResult().hasErrors()) {
|
||||
if (!mavContainer.isBindingDisabled(name)) {
|
||||
bindRequestParameters(binder, webRequest);
|
||||
}
|
||||
validateIfApplicable(binder, parameter);
|
||||
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
|
||||
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
|
||||
}
|
||||
}
|
||||
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
|
||||
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
|
||||
}
|
||||
// Value type adaptation, also covering java.util.Optional
|
||||
if (!parameter.getParameterType().isInstance(attribute)) {
|
||||
|
@ -189,165 +171,60 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
return attribute;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Object wrapAsOptionalIfNecessary(MethodParameter parameter, @Nullable Object target) {
|
||||
return (parameter.getParameterType() == Optional.class ? Optional.ofNullable(target) : target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point to create the model attribute if not found in the model,
|
||||
* with subsequent parameter binding through bean properties (unless suppressed).
|
||||
* <p>The default implementation typically uses the unique public no-arg constructor
|
||||
* if available but also handles a "primary constructor" approach for data classes:
|
||||
* It understands the JavaBeans {@code ConstructorProperties} annotation as well as
|
||||
* runtime-retained parameter names in the bytecode, associating request parameters
|
||||
* with constructor arguments by name. If no such constructor is found, the default
|
||||
* constructor will be used (even if not public), assuming subsequent bean property
|
||||
* bindings through setter methods.
|
||||
* <p>By default, as of 6.1 this method returns {@code null} in which case
|
||||
* {@link org.springframework.validation.DataBinder#construct} is used instead
|
||||
* to create the model attribute. The main purpose of this method then is to
|
||||
* allow to create the model attribute in some other, alternative way.
|
||||
* @param attributeName the name of the attribute (never {@code null})
|
||||
* @param parameter the method parameter declaration
|
||||
* @param binderFactory for creating WebDataBinder instance
|
||||
* @param webRequest the current request
|
||||
* @return the created model attribute (never {@code null})
|
||||
* @throws BindException in case of constructor argument binding failure
|
||||
* @throws Exception in case of constructor invocation failure
|
||||
* @throws Exception in case of constructor instantiation failure
|
||||
* @see #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)
|
||||
* @see BeanUtils#findPrimaryConstructor(Class)
|
||||
*/
|
||||
@Nullable
|
||||
protected Object createAttribute(String attributeName, MethodParameter parameter,
|
||||
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
|
||||
|
||||
MethodParameter nestedParameter = parameter.nestedIfOptional();
|
||||
Class<?> clazz = nestedParameter.getNestedParameterType();
|
||||
|
||||
Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);
|
||||
Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
|
||||
if (parameter != nestedParameter) {
|
||||
attribute = Optional.of(attribute);
|
||||
}
|
||||
return attribute;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new attribute instance with the given constructor.
|
||||
* <p>Called from
|
||||
* {@link #createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
|
||||
* after constructor resolution.
|
||||
* @param ctor the constructor to use
|
||||
* @param attributeName the name of the attribute (never {@code null})
|
||||
* @param parameter the method parameter declaration
|
||||
* @param binderFactory for creating WebDataBinder instance
|
||||
* @param webRequest the current request
|
||||
* @return the created model attribute (never {@code null})
|
||||
* @throws BindException in case of constructor argument binding failure
|
||||
* @throws Exception in case of constructor invocation failure
|
||||
* @since 5.1
|
||||
* @deprecated and not called; replaced by built-in support for
|
||||
* constructor initialization in {@link org.springframework.validation.DataBinder}
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
|
||||
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
|
||||
|
||||
if (ctor.getParameterCount() == 0) {
|
||||
// A single default constructor -> clearly a standard JavaBeans arrangement.
|
||||
return BeanUtils.instantiateClass(ctor);
|
||||
}
|
||||
|
||||
// A single data class constructor -> resolve constructor arguments from request parameters.
|
||||
String[] paramNames = BeanUtils.getParameterNames(ctor);
|
||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
||||
Object[] args = new Object[paramTypes.length];
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName, parameter);
|
||||
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
|
||||
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
|
||||
boolean bindingFailure = false;
|
||||
Set<String> failedParams = new HashSet<>(4);
|
||||
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
String paramName = paramNames[i];
|
||||
Class<?> paramType = paramTypes[i];
|
||||
Object value = webRequest.getParameterValues(paramName);
|
||||
|
||||
// Since WebRequest#getParameter exposes a single-value parameter as an array
|
||||
// with a single element, we unwrap the single value in such cases, analogous
|
||||
// to WebExchangeDataBinder.addBindValue(Map<String, Object>, String, List<?>).
|
||||
if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) {
|
||||
value = Array.get(value, 0);
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
if (fieldDefaultPrefix != null) {
|
||||
value = webRequest.getParameter(fieldDefaultPrefix + paramName);
|
||||
}
|
||||
if (value == null) {
|
||||
if (fieldMarkerPrefix != null && webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
|
||||
value = binder.getEmptyValue(paramType);
|
||||
}
|
||||
else {
|
||||
value = resolveConstructorArgument(paramName, paramType, webRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
MethodParameter methodParam = MethodParameter.forFieldAwareConstructor(ctor, i, paramName);
|
||||
if (value == null && methodParam.isOptional()) {
|
||||
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
|
||||
}
|
||||
else {
|
||||
args[i] = binder.convertIfNecessary(value, paramType, methodParam);
|
||||
}
|
||||
}
|
||||
catch (TypeMismatchException ex) {
|
||||
ex.initPropertyName(paramName);
|
||||
args[i] = null;
|
||||
failedParams.add(paramName);
|
||||
binder.getBindingResult().recordFieldValue(paramName, paramType, value);
|
||||
binder.getBindingErrorProcessor().processPropertyAccessException(ex, binder.getBindingResult());
|
||||
bindingFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingFailure) {
|
||||
BindingResult result = binder.getBindingResult();
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
String paramName = paramNames[i];
|
||||
if (!failedParams.contains(paramName)) {
|
||||
Object value = args[i];
|
||||
result.recordFieldValue(paramName, paramTypes[i], value);
|
||||
validateValueIfApplicable(binder, parameter, ctor.getDeclaringClass(), paramName, value);
|
||||
}
|
||||
}
|
||||
if (!parameter.isOptional()) {
|
||||
try {
|
||||
Object target = BeanUtils.instantiateClass(ctor, args);
|
||||
throw new MethodArgumentNotValidException(parameter, result) {
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
return target;
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (BeanInstantiationException ex) {
|
||||
// swallow and proceed without target instance
|
||||
}
|
||||
}
|
||||
throw new MethodArgumentNotValidException(parameter, result);
|
||||
}
|
||||
|
||||
try {
|
||||
return BeanUtils.instantiateClass(ctor, args);
|
||||
}
|
||||
catch (BeanInstantiationException ex) {
|
||||
if (KotlinDetector.isKotlinType(ctor.getDeclaringClass()) &&
|
||||
ex.getCause() instanceof NullPointerException cause) {
|
||||
BindingResult result = binder.getBindingResult();
|
||||
ObjectError error = new ObjectError(ctor.getName(), cause.getMessage());
|
||||
result.addError(error);
|
||||
throw new MethodArgumentNotValidException(parameter, result);
|
||||
}
|
||||
else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point to bind the request to the target object.
|
||||
* Extension point to create the attribute, binding the request to constructor args.
|
||||
* @param binder the data binder instance to use for the binding
|
||||
* @param request the current request
|
||||
* @since 6.1
|
||||
*/
|
||||
protected void constructAttribute(WebDataBinder binder, NativeWebRequest request) {
|
||||
((WebRequestDataBinder) binder).construct(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point to bind the request to the target object via setters/fields.
|
||||
* @param binder the data binder instance to use for the binding
|
||||
* @param request the current request
|
||||
*/
|
||||
|
@ -355,28 +232,16 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
((WebRequestDataBinder) binder).bind(request);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
/**
|
||||
* Resolve the value for a constructor argument.
|
||||
* @deprecated and not called; replaced by built-in support for
|
||||
* constructor initialization in {@link org.springframework.validation.DataBinder}
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public Object resolveConstructorArgument(String paramName, Class<?> paramType, NativeWebRequest request)
|
||||
throws Exception {
|
||||
|
||||
MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
|
||||
if (multipartRequest != null) {
|
||||
List<MultipartFile> files = multipartRequest.getFiles(paramName);
|
||||
if (!files.isEmpty()) {
|
||||
return (files.size() == 1 ? files.get(0) : files);
|
||||
}
|
||||
}
|
||||
else if (StringUtils.startsWithIgnoreCase(
|
||||
request.getHeader(HttpHeaders.CONTENT_TYPE), MediaType.MULTIPART_FORM_DATA_VALUE)) {
|
||||
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
|
||||
if (servletRequest != null && HttpMethod.POST.matches(servletRequest.getMethod())) {
|
||||
List<Part> parts = StandardServletPartUtils.getParts(servletRequest, paramName);
|
||||
if (!parts.isEmpty()) {
|
||||
return (parts.size() == 1 ? parts.get(0) : parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -401,38 +266,15 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
|
||||
/**
|
||||
* Validate the specified candidate value if applicable.
|
||||
* <p>The default implementation checks for {@code @jakarta.validation.Valid},
|
||||
* Spring's {@link org.springframework.validation.annotation.Validated},
|
||||
* and custom annotations whose name starts with "Valid".
|
||||
* @param binder the DataBinder to be used
|
||||
* @param parameter the method parameter declaration
|
||||
* @param targetType the target type
|
||||
* @param fieldName the name of the field
|
||||
* @param value the candidate value
|
||||
* @since 5.1
|
||||
* @see #validateIfApplicable(WebDataBinder, MethodParameter)
|
||||
* @see SmartValidator#validateValue(Class, String, Object, Errors, Object...)
|
||||
* @deprecated and not called; replaced by built-in support for
|
||||
* constructor initialization in {@link org.springframework.validation.DataBinder}
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter parameter,
|
||||
Class<?> targetType, String fieldName, @Nullable Object value) {
|
||||
|
||||
for (Annotation ann : parameter.getParameterAnnotations()) {
|
||||
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
|
||||
if (validationHints != null) {
|
||||
for (Validator validator : binder.getValidators()) {
|
||||
if (validator instanceof SmartValidator smartValidator) {
|
||||
try {
|
||||
smartValidator.validateValue(targetType, fieldName, value,
|
||||
binder.getBindingResult(), validationHints);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// No corresponding field on the target class...
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.SynthesizingMethodParameter;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.validation.BindingResult;
|
||||
|
@ -51,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.notNull;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
@ -161,11 +162,13 @@ public class ModelAttributeMethodProcessorTests {
|
|||
@Test
|
||||
public void resolveArgumentViaDefaultConstructor() throws Exception {
|
||||
WebDataBinder dataBinder = new WebRequestDataBinder(null);
|
||||
dataBinder.setTargetType(ResolvableType.forMethodParameter(this.paramNamedValidModelAttr));
|
||||
|
||||
WebDataBinderFactory factory = mock();
|
||||
given(factory.createBinder(any(), notNull(), eq("attrName"), any())).willReturn(dataBinder);
|
||||
given(factory.createBinder(any(), isNull(), eq("attrName"), any())).willReturn(dataBinder);
|
||||
|
||||
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
|
||||
verify(factory).createBinder(any(), notNull(), eq("attrName"), any());
|
||||
verify(factory).createBinder(any(), isNull(), eq("attrName"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -281,6 +284,7 @@ public class ModelAttributeMethodProcessorTests {
|
|||
given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"), any()))
|
||||
.willAnswer(invocation -> {
|
||||
WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1));
|
||||
binder.setTargetType(ResolvableType.forMethodParameter(this.beanWithConstructorArgs));
|
||||
// Add conversion service which will convert "1,2" to a list
|
||||
binder.setConversionService(new DefaultFormattingConversionService());
|
||||
return binder;
|
||||
|
|
|
@ -20,10 +20,12 @@ import org.assertj.core.api.Assertions.assertThat
|
|||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.*
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.BDDMockito.given
|
||||
import org.mockito.Mockito.mock
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.core.ResolvableType
|
||||
import org.springframework.core.annotation.SynthesizingMethodParameter
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory
|
||||
|
@ -31,7 +33,6 @@ import org.springframework.web.bind.support.WebRequestDataBinder
|
|||
import org.springframework.web.context.request.ServletWebRequest
|
||||
import org.springframework.web.method.support.ModelAndViewContainer
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest
|
||||
import kotlin.annotation.AnnotationTarget.*
|
||||
|
||||
/**
|
||||
* Kotlin test fixture for [ModelAttributeMethodProcessor].
|
||||
|
@ -61,7 +62,11 @@ class ModelAttributeMethodProcessorKotlinTests {
|
|||
val requestWithParam = ServletWebRequest(mockRequest)
|
||||
val factory = mock<WebDataBinderFactory>()
|
||||
given(factory.createBinder(any(), any(), eq("param"), any()))
|
||||
.willAnswer { WebRequestDataBinder(it.getArgument(1)) }
|
||||
.willAnswer {
|
||||
val binder = WebRequestDataBinder(it.getArgument(1))
|
||||
binder.setTargetType(ResolvableType.forMethodParameter(this.param))
|
||||
binder
|
||||
}
|
||||
assertThat(processor.resolveArgument(this.param, container, requestWithParam, factory)).isEqualTo(Param("b"))
|
||||
}
|
||||
|
||||
|
@ -71,7 +76,11 @@ class ModelAttributeMethodProcessorKotlinTests {
|
|||
val requestWithParam = ServletWebRequest(mockRequest)
|
||||
val factory = mock<WebDataBinderFactory>()
|
||||
given(factory.createBinder(any(), any(), eq("param"), any()))
|
||||
.willAnswer { WebRequestDataBinder(it.getArgument(1)) }
|
||||
.willAnswer {
|
||||
val binder = WebRequestDataBinder(it.getArgument(1))
|
||||
binder.setTargetType(ResolvableType.forMethodParameter(this.param))
|
||||
binder
|
||||
}
|
||||
assertThatThrownBy {
|
||||
processor.resolveArgument(this.param, container, requestWithParam, factory)
|
||||
}.isInstanceOf(MethodArgumentNotValidException::class.java)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -23,6 +23,7 @@ import jakarta.servlet.ServletRequest;
|
|||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.ServletRequestDataBinder;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
/**
|
||||
|
@ -67,14 +68,17 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected ServletRequestValueResolver createValueResolver(ServletRequest request) {
|
||||
return new ExtendedServletRequestValueResolver(request, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge URI variables into the property values to use for data binding.
|
||||
*/
|
||||
@Override
|
||||
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);
|
||||
Map<String, String> uriVars = getUriVars(request);
|
||||
if (uriVars != null) {
|
||||
uriVars.forEach((name, value) -> {
|
||||
if (mpvs.contains(name)) {
|
||||
|
@ -89,4 +93,34 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private static Map<String, String> getUriVars(ServletRequest request) {
|
||||
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
|
||||
return (Map<String, String>) request.getAttribute(attr);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolver of values that looks up URI path variables.
|
||||
*/
|
||||
private static class ExtendedServletRequestValueResolver extends ServletRequestValueResolver {
|
||||
|
||||
ExtendedServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) {
|
||||
super(request, dataBinder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getRequestParameter(String name, Class<?> type) {
|
||||
Object value = super.getRequestParameter(name, type);
|
||||
if (value == null) {
|
||||
Map<String, String> uriVars = getUriVars(getRequest());
|
||||
if (uriVars != null) {
|
||||
value = uriVars.get(name);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -146,9 +146,18 @@ public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodPr
|
|||
}
|
||||
|
||||
/**
|
||||
* This implementation downcasts {@link WebDataBinder} to
|
||||
* {@link ServletRequestDataBinder} before binding.
|
||||
* @see ServletRequestDataBinderFactory
|
||||
* Downcast to {@link ServletRequestDataBinder} to invoke {@code constructTarget(ServletRequest)}.
|
||||
*/
|
||||
@Override
|
||||
protected void constructAttribute(WebDataBinder binder, NativeWebRequest request) {
|
||||
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
|
||||
Assert.state(servletRequest != null, "No ServletRequest");
|
||||
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
|
||||
servletBinder.construct(servletRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downcast to {@link ServletRequestDataBinder} to invoke {@code bind(ServletRequest)}.
|
||||
*/
|
||||
@Override
|
||||
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
|
||||
|
@ -158,23 +167,4 @@ 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue