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:
Rossen Stoyanchev
2023-06-29 13:15:04 +01:00
committed by rstoyanchev
parent e83594a2a3
commit ba4d9a5230
5 changed files with 166 additions and 76 deletions

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}
}
}