Consistent processing of binding/validation failures for data classes

Includes an extension of SmartValidator for candidate value validation, as well as nullability refinements in Validator and BindingResult.

Issue: SPR-16840
Issue: SPR-16841
Issue: SPR-16854
This commit is contained in:
Juergen Hoeller 2018-07-17 17:01:34 +02:00
parent d8a2927dd3
commit 955665b419
12 changed files with 341 additions and 99 deletions

View File

@ -222,7 +222,7 @@ public abstract class AbstractBindingResult extends AbstractErrors implements Bi
if (fieldError != null) {
Object value = fieldError.getRejectedValue();
// Do not apply formatting on binding failures like type mismatches.
return (fieldError.isBindingFailure() ? value : formatFieldValue(field, value));
return (fieldError.isBindingFailure() || getTarget() == null ? value : formatFieldValue(field, value));
}
else if (getTarget() != null) {
Object value = getActualFieldValue(fixedField(field));
@ -321,9 +321,8 @@ public abstract class AbstractBindingResult extends AbstractErrors implements Bi
@Override
public String[] resolveMessageCodes(String errorCode, @Nullable String field) {
Class<?> fieldType = (getTarget() != null ? getFieldType(field) : null);
return getMessageCodesResolver().resolveMessageCodes(
errorCode, getObjectName(), fixedField(field), fieldType);
errorCode, getObjectName(), fixedField(field), getFieldType(field));
}
@Override
@ -332,7 +331,7 @@ public abstract class AbstractBindingResult extends AbstractErrors implements Bi
}
@Override
public void recordFieldValue(String field, Class<?> type, Object value) {
public void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
this.fieldTypes.put(field, type);
this.fieldValues.put(field, value);
}

View File

@ -275,7 +275,7 @@ public class BindException extends Exception implements BindingResult {
}
@Override
public void recordFieldValue(String field, Class<?> type, Object value) {
public void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
this.bindingResult.recordFieldValue(field, type, value);
}

View File

@ -143,7 +143,7 @@ public interface BindingResult extends Errors {
* @param value the original value
* @since 5.0.4
*/
default void recordFieldValue(String field, Class<?> type, Object value) {
default void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
}
/**

View File

@ -853,8 +853,12 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* @see #getBindingResult()
*/
public void validate() {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
for (Validator validator : this.validators) {
validator.validate(getTarget(), getBindingResult());
validator.validate(target, bindingResult);
}
}
@ -862,16 +866,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* Invoke the specified Validators, if any, with the given validation hints.
* <p>Note: Validation hints may get ignored by the actual target Validator.
* @param validationHints one or more hint objects to be passed to a {@link SmartValidator}
* @since 3.1
* @see #setValidator(Validator)
* @see SmartValidator#validate(Object, Errors, Object...)
*/
public void validate(Object... validationHints) {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(getTarget(), getBindingResult(), validationHints);
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(getTarget(), getBindingResult());
validator.validate(target, bindingResult);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2018 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.
@ -39,11 +39,29 @@ public interface SmartValidator extends Validator {
* <p>Note: Validation hints may get ignored by the actual target {@code Validator},
* in which case this method should behave just like its regular
* {@link #validate(Object, Errors)} sibling.
* @param target the object that is to be validated (can be {@code null})
* @param errors contextual state about the validation process (never {@code null})
* @param target the object that is to be validated
* @param errors contextual state about the validation process
* @param validationHints one or more hint objects to be passed to the validation engine
* @see ValidationUtils
* @see javax.validation.Validator#validate(Object, Class[])
*/
void validate(@Nullable Object target, Errors errors, Object... validationHints);
void validate(Object target, Errors errors, Object... validationHints);
/**
* Validate the supplied value for the specified field on the target type,
* reporting the same validation errors as if the value would be bound to
* the field on an instance of the target class.
* @param targetType the target type
* @param fieldName the name of the field
* @param value the candidate value
* @param errors contextual state about the validation process
* @param validationHints one or more hint objects to be passed to the validation engine
* @since 5.1
* @see javax.validation.Validator#validateValue(Class, String, Object, Class[])
*/
default void validateValue(
Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
throw new IllegalArgumentException("Cannot validate individual value for " + targetType);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@ -45,30 +45,30 @@ public abstract class ValidationUtils {
/**
* Invoke the given {@link Validator} for the supplied object and
* {@link Errors} instance.
* @param validator the {@code Validator} to be invoked (must not be {@code null})
* @param obj the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors (must not be {@code null})
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors} arguments is
* {@code null}, or if the supplied {@code Validator} does not {@link Validator#supports(Class) support}
* the validation of the supplied object's type
* @param validator the {@code Validator} to be invoked
* @param target the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors}
* arguments is {@code null}, or if the supplied {@code Validator} does not
* {@link Validator#supports(Class) support} the validation of the supplied object's type
*/
public static void invokeValidator(Validator validator, Object obj, Errors errors) {
invokeValidator(validator, obj, errors, (Object[]) null);
public static void invokeValidator(Validator validator, Object target, Errors errors) {
invokeValidator(validator, target, errors, (Object[]) null);
}
/**
* Invoke the given {@link Validator}/{@link SmartValidator} for the supplied object and
* {@link Errors} instance.
* @param validator the {@code Validator} to be invoked (must not be {@code null})
* @param obj the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors (must not be {@code null})
* @param validator the {@code Validator} to be invoked
* @param target the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors
* @param validationHints one or more hint objects to be passed to the validation engine
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors} arguments is
* {@code null}, or if the supplied {@code Validator} does not {@link Validator#supports(Class) support}
* the validation of the supplied object's type
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors}
* arguments is {@code null}, or if the supplied {@code Validator} does not
* {@link Validator#supports(Class) support} the validation of the supplied object's type
*/
public static void invokeValidator(
Validator validator, @Nullable Object obj, Errors errors, @Nullable Object... validationHints) {
Validator validator, Object target, Errors errors, @Nullable Object... validationHints) {
Assert.notNull(validator, "Validator must not be null");
Assert.notNull(errors, "Errors object must not be null");
@ -76,16 +76,16 @@ public abstract class ValidationUtils {
if (logger.isDebugEnabled()) {
logger.debug("Invoking validator [" + validator + "]");
}
if (obj != null && !validator.supports(obj.getClass())) {
if (!validator.supports(target.getClass())) {
throw new IllegalArgumentException(
"Validator [" + validator.getClass() + "] does not support [" + obj.getClass() + "]");
"Validator [" + validator.getClass() + "] does not support [" + target.getClass() + "]");
}
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(obj, errors, validationHints);
((SmartValidator) validator).validate(target, errors, validationHints);
}
else {
validator.validate(obj, errors);
validator.validate(target, errors);
}
if (logger.isDebugEnabled()) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2018 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,8 +16,6 @@
package org.springframework.validation;
import org.springframework.lang.Nullable;
/**
* A validator for application-specific objects.
*
@ -61,6 +59,7 @@ import org.springframework.lang.Nullable;
* application.
*
* @author Rod Johnson
* @see SmartValidator
* @see Errors
* @see ValidationUtils
*/
@ -87,10 +86,10 @@ public interface Validator {
* typically has (or would) return {@code true}.
* <p>The supplied {@link Errors errors} instance can be used to report
* any resulting validation errors.
* @param target the object that is to be validated (can be {@code null})
* @param errors contextual state about the validation process (never {@code null})
* @param target the object that is to be validated
* @param errors contextual state about the validation process
* @see ValidationUtils
*/
void validate(@Nullable Object target, Errors errors);
void validate(Object target, Errors errors);
}

View File

@ -50,13 +50,17 @@ import org.springframework.validation.SmartValidator;
* while also exposing the original JSR-303 Validator interface itself.
*
* <p>Can be used as a programmatic wrapper. Also serves as base class for
* {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean}.
* {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean},
* and as the primary implementation of the {@link SmartValidator} interface.
*
* <p>As of Spring Framework 5.0, this adapter is fully compatible with
* Bean Validation 1.1 as well as 2.0.
*
* @author Juergen Hoeller
* @since 3.0
* @see SmartValidator
* @see CustomValidatorBean
* @see LocalValidatorFactoryBean
*/
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {
@ -99,28 +103,45 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation.
}
@Override
public void validate(@Nullable Object target, Errors errors) {
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validate(target), errors);
}
}
@Override
public void validate(@Nullable Object target, Errors errors, @Nullable Object... validationHints) {
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
Set<Class<?>> groups = new LinkedHashSet<>();
if (validationHints != null) {
for (Object hint : validationHints) {
if (hint instanceof Class) {
groups.add((Class<?>) hint);
}
}
}
processConstraintViolations(
this.targetValidator.validate(target, ClassUtils.toClassArray(groups)), errors);
this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
}
}
@SuppressWarnings("unchecked")
@Override
public void validateValue(
Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validateValue(
(Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors);
}
}
/**
* Turn the specified validation hints into JSR-303 validation groups.
* @since 5.1
*/
private Class<?>[] asValidationGroups(Object... validationHints) {
Set<Class<?>> groups = new LinkedHashSet<>(4);
for (Object hint : validationHints) {
if (hint instanceof Class) {
groups.add((Class<?>) hint);
}
}
return ClassUtils.toClassArray(groups);
}
/**
* Process the given JSR-303 ConstraintViolations, adding corresponding errors to
* the provided Spring {@link Errors} object.

View File

@ -261,7 +261,7 @@ public class WebExchangeBindException extends ServerWebInputException implements
}
@Override
public void recordFieldValue(String field, Class<?> type, Object value) {
public void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
this.bindingResult.recordFieldValue(field, type, value);
}

View File

@ -19,8 +19,14 @@ package org.springframework.web.method.annotation;
import java.beans.ConstructorProperties;
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -33,10 +39,11 @@ import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.validation.AbstractBindingResult;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
@ -189,7 +196,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
* @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
* @see #constructAttribute(Constructor, String, WebDataBinderFactory, NativeWebRequest)
* @see #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)
* @see BeanUtils#findPrimaryConstructor(Class)
*/
protected Object createAttribute(String attributeName, MethodParameter parameter,
@ -214,7 +221,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
}
}
Object attribute = constructAttribute(ctor, attributeName, binderFactory, webRequest);
Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
if (parameter != nestedParameter) {
attribute = Optional.of(attribute);
}
@ -233,11 +240,17 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
* @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.0
* @since 5.1
*/
protected Object constructAttribute(Constructor<?> ctor, String attributeName,
@SuppressWarnings("deprecation")
protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
Object constructed = constructAttribute(ctor, attributeName, binderFactory, webRequest);
if (constructed != null) {
return constructed;
}
if (ctor.getParameterCount() == 0) {
// A single default constructor -> clearly a standard JavaBeans arrangement.
return BeanUtils.instantiateClass(ctor);
@ -256,6 +269,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
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];
@ -272,7 +286,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
}
}
try {
MethodParameter methodParam = new MethodParameter(ctor, i);
MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName);
if (value == null && methodParam.isOptional()) {
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
}
@ -282,25 +296,43 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
}
catch (TypeMismatchException ex) {
ex.initPropertyName(paramName);
args[i] = value;
failedParams.add(paramName);
binder.getBindingResult().recordFieldValue(paramName, paramType, value);
binder.getBindingErrorProcessor().processPropertyAccessException(ex, binder.getBindingResult());
bindingFailure = true;
args[i] = value;
}
}
if (bindingFailure) {
if (binder.getBindingResult() instanceof AbstractBindingResult) {
AbstractBindingResult result = (AbstractBindingResult) binder.getBindingResult();
for (int i = 0; i < paramNames.length; i++) {
result.recordFieldValue(paramNames[i], paramTypes[i], args[i]);
BindingResult result = binder.getBindingResult();
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
if (!failedParams.contains(paramName)) {
result.recordFieldValue(paramName, paramTypes[i], args[i]);
validateValueIfApplicable(binder, parameter, ctor.getDeclaringClass(), paramName, args[i]);
}
}
throw new BindException(binder.getBindingResult());
throw new BindException(result);
}
return BeanUtils.instantiateClass(ctor, args);
}
/**
* Construct a new attribute instance with the given constructor.
* @since 5.0
* @deprecated as of 5.1, in favor of
* {@link #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
*/
@Deprecated
@Nullable
protected Object constructAttribute(Constructor<?> ctor, String attributeName,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
return null;
}
/**
* Extension point to bind the request to the target object.
* @param binder the data binder instance to use for the binding
@ -317,20 +349,72 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
* and custom annotations whose name starts with "Valid".
* @param binder the DataBinder to be used
* @param parameter the method parameter declaration
* @see WebDataBinder#validate(Object...)
* @see SmartValidator#validate(Object, Errors, Object...)
*/
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
for (Annotation ann : parameter.getParameterAnnotations()) {
Object[] validationHints = determineValidationHints(ann);
if (validationHints != null) {
binder.validate(validationHints);
break;
}
}
}
/**
* Validate the specified candidate value if applicable.
* <p>The default implementation checks for {@code @javax.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...)
*/
protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter parameter,
Class<?> targetType, String fieldName, @Nullable Object value) {
for (Annotation ann : parameter.getParameterAnnotations()) {
Object[] validationHints = determineValidationHints(ann);
if (validationHints != null) {
for (Validator validator : binder.getValidators()) {
if (validator instanceof SmartValidator) {
try {
((SmartValidator) validator).validateValue(targetType, fieldName, value,
binder.getBindingResult(), validationHints);
}
catch (IllegalArgumentException ex) {
// No corresponding field on the target class...
}
}
}
break;
}
}
}
/**
* Determine any validation triggered by the given annotation.
* @param ann the annotation (potentially a validation annotation)
* @return the validation hints to apply (possibly an empty array),
* or {@code null} if this annotation does not trigger any validation
* @since 5.1
*/
@Nullable
private Object[] determineValidationHints(Annotation ann) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
}
return null;
}
/**
* Whether to raise a fatal bind exception on validation errors.
* <p>The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}.
@ -380,4 +464,61 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
}
}
/**
* {@link MethodParameter} subclass which detects field annotations as well.
* @since 5.1
*/
private static class FieldAwareConstructorParameter extends MethodParameter {
private final String parameterName;
@Nullable
private volatile Annotation[] combinedAnnotations;
public FieldAwareConstructorParameter(Constructor<?> constructor, int parameterIndex, String parameterName) {
super(constructor, parameterIndex);
this.parameterName = parameterName;
}
@Override
public Annotation[] getParameterAnnotations() {
Annotation[] anns = this.combinedAnnotations;
if (anns == null) {
anns = super.getParameterAnnotations();
try {
Field field = getDeclaringClass().getDeclaredField(this.parameterName);
Annotation[] fieldAnns = field.getAnnotations();
if (fieldAnns.length > 0) {
List<Annotation> merged = new ArrayList<>(anns.length + fieldAnns.length);
merged.addAll(Arrays.asList(anns));
for (Annotation fieldAnn : fieldAnns) {
boolean existingType = false;
for (Annotation ann : anns) {
if (ann.annotationType() == fieldAnn.annotationType()) {
existingType = true;
break;
}
}
if (!existingType) {
merged.add(fieldAnn);
}
}
anns = merged.toArray(new Annotation[0]);
}
}
catch (NoSuchFieldException | SecurityException ex) {
// ignore
}
this.combinedAnnotations = anns;
}
return anns;
}
@Override
public String getParameterName() {
return this.parameterName;
}
}
}

View File

@ -160,9 +160,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
}
catch (Exception ex) {
// Leave stack trace for later, e.g. AbstractHandlerExceptionResolver
String message = ex.getMessage();
if (!message.contains(parameter.getExecutable().toGenericString())) {
if (logger.isDebugEnabled()) {
if (logger.isDebugEnabled()) {
String message = ex.getMessage();
if (message != null && !message.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, message));
}
}

View File

@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -72,6 +73,7 @@ import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
import org.springframework.http.HttpEntity;
@ -1863,7 +1865,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
request.addParameter("param1", "value1");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("value1-null-null", response.getContentAsString());
assertEquals("1:value1-null-null", response.getContentAsString());
}
@Test
@ -1875,7 +1877,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
request.addParameter("param2", "x");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("value1-x-null", response.getContentAsString());
assertEquals("1:value1-x-null", response.getContentAsString());
}
@Test
@ -1884,10 +1886,21 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
request.addParameter("param2", "true");
request.addParameter("param3", "3");
request.addParameter("param3", "0");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("null-true-3", response.getContentAsString());
assertEquals("1:null-true-0", response.getContentAsString());
}
@Test
public void dataClassBindingWithValidationErrorAndConversionError() throws Exception {
initServletWithControllers(ValidatedDataClassController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
request.addParameter("param2", "x");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("2:null-x-null", response.getContentAsString());
}
@Test
@ -1965,6 +1978,17 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertEquals("value1-false-0", response.getContentAsString());
}
@Test
public void dataClassBindingWithLocalDate() throws Exception {
initServletWithControllers(DateClassController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
request.addParameter("date", "2010-01-01");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("2010-01-01", response.getContentAsString());
}
@Controller
static class ControllerWithEmptyValueMapping {
@ -2915,14 +2939,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
@Override
public Object read(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
throw new HttpMessageNotReadableException("Could not read");
public Object read(Class<?> clazz, HttpInputMessage inputMessage) {
throw new HttpMessageNotReadableException("Could not read", inputMessage);
}
@Override
public void write(Object o, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
public void write(Object o, @Nullable MediaType contentType, HttpOutputMessage outputMessage) {
throw new UnsupportedOperationException("Not implemented");
}
}
@ -3672,28 +3694,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@RequestMapping("/bind")
public BindStatusView handle(@Valid DataClass data, BindingResult result) {
if (result.hasErrors()) {
return new BindStatusView(result.getFieldValue("param1") + "-" +
return new BindStatusView(result.getErrorCount() + ":" + result.getFieldValue("param1") + "-" +
result.getFieldValue("param2") + "-" + result.getFieldValue("param3"));
}
return new BindStatusView(data.param1 + "-" + data.param2 + "-" + data.param3);
}
}
@RestController
public static class OptionalDataClassController {
@RequestMapping("/bind")
public String handle(Optional<DataClass> optionalData, BindingResult result) {
if (result.hasErrors()) {
assertNotNull(optionalData);
assertFalse(optionalData.isPresent());
return result.getFieldValue("param1") + "-" + result.getFieldValue("param2") + "-" +
result.getFieldValue("param3");
}
return optionalData.map(data -> data.param1 + "-" + data.param2 + "-" + data.param3).orElse("");
}
}
public static class BindStatusView extends AbstractView {
private final String content;
@ -3714,4 +3721,52 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
}
@RestController
public static class OptionalDataClassController {
@RequestMapping("/bind")
public String handle(Optional<DataClass> optionalData, BindingResult result) {
if (result.hasErrors()) {
assertNotNull(optionalData);
assertFalse(optionalData.isPresent());
return result.getFieldValue("param1") + "-" + result.getFieldValue("param2") + "-" +
result.getFieldValue("param3");
}
return optionalData.map(data -> data.param1 + "-" + data.param2 + "-" + data.param3).orElse("");
}
}
public static class DateClass {
@DateTimeFormat(pattern = "yyyy-MM-dd")
public LocalDate date;
public DateClass(LocalDate date) {
this.date = date;
}
}
@RestController
public static class DateClassController {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.initDirectFieldAccess();
binder.setConversionService(new DefaultFormattingConversionService());
}
@RequestMapping("/bind")
public String handle(DateClass data, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().toString();
}
assertNotNull(data);
assertNotNull(data.date);
assertEquals(2010, data.date.getYear());
assertEquals(1, data.date.getMonthValue());
assertEquals(1, data.date.getDayOfMonth());
return result.getFieldValue("date").toString();
}
}
}