Add BindErrorUtils
This deprecates static methods in MethodArgumentNotValidException which is not a great vehicle for such methods. See gh-30644
This commit is contained in:
parent
e83594a2a3
commit
ba4d9a5230
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package org.springframework.web.bind;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
|
@ -27,13 +26,11 @@ import org.springframework.http.HttpStatus;
|
|||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.util.BindErrorUtils;
|
||||
|
||||
/**
|
||||
* Exception to be thrown when validation on an argument annotated with {@code @Valid} fails.
|
||||
|
|
@ -82,57 +79,43 @@ public class MethodArgumentNotValidException extends BindException implements Er
|
|||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
public Object[] getDetailMessageArguments(MessageSource source, Locale locale) {
|
||||
return new Object[] {
|
||||
join(errorsToStringList(getGlobalErrors())),
|
||||
join(errorsToStringList(getFieldErrors()))};
|
||||
BindErrorUtils.resolveAndJoin(getGlobalErrors(), source, locale),
|
||||
BindErrorUtils.resolveAndJoin(getFieldErrors(), source, locale)};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments(MessageSource messageSource, Locale locale) {
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return new Object[] {
|
||||
join(errorsToStringList(getGlobalErrors(), messageSource, locale)),
|
||||
join(errorsToStringList(getFieldErrors(), messageSource, locale))};
|
||||
}
|
||||
|
||||
private static String join(List<String> errors) {
|
||||
return String.join(", and ", errors);
|
||||
BindErrorUtils.resolveAndJoin(getGlobalErrors()),
|
||||
BindErrorUtils.resolveAndJoin(getFieldErrors())};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert each given {@link ObjectError} to a String in single quotes, taking
|
||||
* either the error's default message, or its error code.
|
||||
* Convert each given {@link ObjectError} to a String.
|
||||
* @since 6.0
|
||||
* @deprecated in favor of using {@link BindErrorUtils} and
|
||||
* {@link #getAllErrors()}, to be removed in 6.2
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public static List<String> errorsToStringList(List<? extends ObjectError> errors) {
|
||||
return errorsToStringList(errors, null, null);
|
||||
return BindErrorUtils.resolve(errors).values().stream().toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #errorsToStringList(List)} that uses a
|
||||
* {@link MessageSource} to resolve the message code of the error, or fall
|
||||
* back on the error's default message.
|
||||
* Convert each given {@link ObjectError} to a String, and use a
|
||||
* {@link MessageSource} to resolve each error.
|
||||
* @since 6.0
|
||||
* @deprecated in favor of {@link BindErrorUtils}, to be removed in 6.2
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public static List<String> errorsToStringList(
|
||||
List<? extends ObjectError> errors, @Nullable MessageSource messageSource, @Nullable Locale locale) {
|
||||
List<? extends ObjectError> errors, @Nullable MessageSource messageSource, Locale locale) {
|
||||
|
||||
return errors.stream()
|
||||
.map(error -> formatError(error, messageSource, locale))
|
||||
.filter(StringUtils::hasText)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static String formatError(
|
||||
ObjectError error, @Nullable MessageSource messageSource, @Nullable Locale locale) {
|
||||
|
||||
if (messageSource != null) {
|
||||
Assert.notNull(locale, "Expected MessageSource and locale");
|
||||
return messageSource.getMessage(error, locale);
|
||||
}
|
||||
String field = (error instanceof FieldError fieldError ? fieldError.getField() + ": " : "");
|
||||
String message = (error.getDefaultMessage() != null ? error.getDefaultMessage() : error.getCode());
|
||||
return (field + message);
|
||||
return (messageSource != null ?
|
||||
BindErrorUtils.resolve(errors, messageSource, locale).values().stream().toList() :
|
||||
BindErrorUtils.resolve(errors).values().stream().toList());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,12 +123,12 @@ public class MethodArgumentNotValidException extends BindException implements Er
|
|||
* {@link MessageSource} and {@link Locale}.
|
||||
* @return a Map with errors as keys and resolved messages as values
|
||||
* @since 6.0.3
|
||||
* @deprecated in favor of using {@link BindErrorUtils} and
|
||||
* {@link #getAllErrors()}, to be removed in 6.2
|
||||
*/
|
||||
public Map<ObjectError, String> resolveErrorMessages(MessageSource source, Locale locale) {
|
||||
Map<ObjectError, String> map = new LinkedHashMap<>(getErrorCount());
|
||||
getGlobalErrors().forEach(error -> map.put(error, formatError(error, source, locale)));
|
||||
getFieldErrors().forEach(error -> map.put(error, formatError(error, source, locale)));
|
||||
return map;
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public Map<ObjectError, String> resolveErrorMessages(MessageSource messageSource, Locale locale) {
|
||||
return BindErrorUtils.resolve(getAllErrors(), messageSource, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package org.springframework.web.bind.support;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
|
@ -32,8 +31,8 @@ import org.springframework.validation.BindingResult;
|
|||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.util.BindErrorUtils;
|
||||
|
||||
/**
|
||||
* {@link ServerWebInputException} subclass that indicates a data binding or
|
||||
|
|
@ -68,20 +67,15 @@ public class WebExchangeBindException extends ServerWebInputException implements
|
|||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return new Object[] {
|
||||
join(MethodArgumentNotValidException.errorsToStringList(getGlobalErrors())),
|
||||
join(MethodArgumentNotValidException.errorsToStringList(getFieldErrors()))};
|
||||
BindErrorUtils.resolveAndJoin(getGlobalErrors()),
|
||||
BindErrorUtils.resolveAndJoin(getFieldErrors())};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments(MessageSource source, Locale locale) {
|
||||
return new Object[] {
|
||||
join(MethodArgumentNotValidException.errorsToStringList(getGlobalErrors(), source, locale)),
|
||||
join(MethodArgumentNotValidException.errorsToStringList(getFieldErrors(), source, locale))
|
||||
};
|
||||
}
|
||||
|
||||
private static String join(List<String> errors) {
|
||||
return String.join(", and ", errors);
|
||||
BindErrorUtils.resolveAndJoin(getGlobalErrors(), source, locale),
|
||||
BindErrorUtils.resolveAndJoin(getFieldErrors(), source, locale)};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,22 +83,12 @@ public class WebExchangeBindException extends ServerWebInputException implements
|
|||
* {@link MessageSource} and {@link Locale}.
|
||||
* @return a Map with errors as key and resolves messages as value
|
||||
* @since 6.0.3
|
||||
* @deprecated in favor of using {@link BindErrorUtils} and
|
||||
* {@link #getAllErrors()}, to be removed in 6.2
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public Map<ObjectError, String> resolveErrorMessages(MessageSource messageSource, Locale locale) {
|
||||
Map<ObjectError, String> map = new LinkedHashMap<>();
|
||||
addMessages(map, getGlobalErrors(), messageSource, locale);
|
||||
addMessages(map, getFieldErrors(), messageSource, locale);
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void addMessages(
|
||||
Map<ObjectError, String> map, List<? extends ObjectError> errors,
|
||||
MessageSource messageSource, Locale locale) {
|
||||
|
||||
List<String> messages = MethodArgumentNotValidException.errorsToStringList(errors, messageSource, locale);
|
||||
for (int i = 0; i < errors.size(); i++) {
|
||||
map.put(errors.get(i), messages.get(i));
|
||||
}
|
||||
return BindErrorUtils.resolve(getAllErrors(), messageSource, locale);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.util;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.support.StaticMessageSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.FieldError;
|
||||
|
||||
/**
|
||||
* Utility methods to resolve list of {@link MessageSourceResolvable}s, and
|
||||
* optionally join them.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.1
|
||||
*/
|
||||
public abstract class BindErrorUtils {
|
||||
|
||||
private final static MessageSource defaultMessageSource = new MethodArgumentErrorMessageSource();
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for {@link #resolveAndJoin(List, MessageSource, Locale)} with
|
||||
* an empty * {@link MessageSource} that simply formats the default message,
|
||||
* or first error code, also prepending the field name for field errors.
|
||||
*/
|
||||
public static String resolveAndJoin(List<? extends MessageSourceResolvable> errors) {
|
||||
return resolveAndJoin(errors, defaultMessageSource, Locale.getDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for {@link #resolveAndJoin(CharSequence, CharSequence, CharSequence, List, MessageSource, Locale)}
|
||||
* with {@code ", and "} as delimiter, and an empty prefix and suffix.
|
||||
*/
|
||||
public static String resolveAndJoin(
|
||||
List<? extends MessageSourceResolvable> errors, MessageSource messageSource, Locale locale) {
|
||||
|
||||
return resolveAndJoin(", and ", "", "", errors, messageSource, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all errors through the given {@link MessageSource} and join them.
|
||||
* @param delimiter the delimiter to use between each error
|
||||
* @param prefix characters to insert at the beginning
|
||||
* @param suffix characters to insert at the end
|
||||
* @param errors the errors to resolve and join
|
||||
* @param messageSource the {@code MessageSource} to resolve with
|
||||
* @param locale the locale to resolve with
|
||||
* @return the resolved errors formatted as a string
|
||||
*/
|
||||
public static String resolveAndJoin(
|
||||
CharSequence delimiter, CharSequence prefix, CharSequence suffix,
|
||||
List<? extends MessageSourceResolvable> errors, MessageSource messageSource, Locale locale) {
|
||||
|
||||
return errors.stream()
|
||||
.map(error -> messageSource.getMessage(error, locale))
|
||||
.filter(StringUtils::hasText)
|
||||
.collect(Collectors.joining(delimiter, prefix, suffix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for {@link #resolve(List, MessageSource, Locale)} with an empty
|
||||
* {@link MessageSource} that simply formats the default message, or first
|
||||
* error code, also prepending the field name for field errors.
|
||||
*/
|
||||
public static <E extends MessageSourceResolvable> Map<E, String> resolve(List<E> errors) {
|
||||
return resolve(errors, defaultMessageSource, Locale.getDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all errors through the given {@link MessageSource}.
|
||||
* @param errors the errors to resolve
|
||||
* @param messageSource the {@code MessageSource} to resolve with
|
||||
* @param locale the locale to resolve with an empty {@link MessageSource}
|
||||
* @return map with resolved errors as values, in the order of the input list
|
||||
*/
|
||||
public static <E extends MessageSourceResolvable> Map<E, String> resolve(
|
||||
List<E> errors, MessageSource messageSource, Locale locale) {
|
||||
|
||||
Map<E, String> map = new LinkedHashMap<>(errors.size());
|
||||
errors.forEach(error -> map.put(error, messageSource.getMessage(error, locale)));
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@code MessageSource} for default error formatting.
|
||||
*/
|
||||
private static class MethodArgumentErrorMessageSource extends StaticMessageSource {
|
||||
|
||||
MethodArgumentErrorMessageSource() {
|
||||
setUseCodeAsDefaultMessage(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
|
||||
String message = super.getDefaultMessage(resolvable, locale);
|
||||
return (resolvable instanceof FieldError error ? error.getField() + ": " + message : message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -56,6 +56,7 @@ import org.springframework.web.server.ServerErrorException;
|
|||
import org.springframework.web.server.UnsatisfiedRequestParameterException;
|
||||
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
|
||||
import org.springframework.web.testfixture.method.ResolvableMethod;
|
||||
import org.springframework.web.util.BindErrorUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
|
@ -252,7 +253,8 @@ public class ErrorResponseExceptionTests {
|
|||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Invalid request content.");
|
||||
messageSourceHelper.assertDetailMessage(ex);
|
||||
messageSourceHelper.assertErrorMessages(ex::resolveErrorMessages);
|
||||
messageSourceHelper.assertErrorMessages(
|
||||
(source, locale) -> BindErrorUtils.resolve(ex.getAllErrors(), source, locale));
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
|
@ -457,8 +459,8 @@ public class ErrorResponseExceptionTests {
|
|||
ex.getDetailMessageCode(), ex.getDetailMessageArguments(), Locale.UK);
|
||||
|
||||
assertThat(message).isEqualTo(
|
||||
"Failed because Invalid bean message, and bean.invalid.B. " +
|
||||
"Also because name: must be provided, and age: age.min");
|
||||
"Failed because Invalid bean message, and bean.invalid.B.myBean. " +
|
||||
"Also because name: must be provided, and age: age.min.myBean.age");
|
||||
|
||||
message = messageSource.getMessage(
|
||||
ex.getDetailMessageCode(), ex.getDetailMessageArguments(messageSource, Locale.UK), Locale.UK);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package org.springframework.web.bind;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
|
@ -29,9 +29,9 @@ import org.springframework.context.support.StaticMessageSource;
|
|||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
||||
import org.springframework.web.util.BindErrorUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
|
@ -47,8 +47,7 @@ public class MethodArgumentNotValidExceptionTests {
|
|||
Person frederick1234 = new Person("Frederick1234", 24);
|
||||
MethodArgumentNotValidException ex = createException(frederick1234);
|
||||
|
||||
List<FieldError> fieldErrors = ex.getFieldErrors();
|
||||
List<String> errors = MethodArgumentNotValidException.errorsToStringList(fieldErrors);
|
||||
Collection<String> errors = BindErrorUtils.resolve(ex.getFieldErrors()).values();
|
||||
|
||||
assertThat(errors).containsExactlyInAnyOrder(
|
||||
"name: size must be between 0 and 10", "age: must be greater than or equal to 25");
|
||||
|
|
@ -63,8 +62,7 @@ public class MethodArgumentNotValidExceptionTests {
|
|||
source.addMessage("Size.name", Locale.UK, "name exceeds {1} characters");
|
||||
source.addMessage("Min.age", Locale.UK, "age is under {1}");
|
||||
|
||||
List<FieldError> fieldErrors = ex.getFieldErrors();
|
||||
List<String> errors = MethodArgumentNotValidException.errorsToStringList(fieldErrors, source, Locale.UK);
|
||||
Collection<String> errors = BindErrorUtils.resolve(ex.getFieldErrors(), source, Locale.UK).values();
|
||||
|
||||
assertThat(errors).containsExactlyInAnyOrder("name exceeds 10 characters", "age is under 25");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue