diff --git a/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java b/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java index 65bcfaf68b57..cafd60a4e86b 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java @@ -17,8 +17,10 @@ package org.springframework.web.bind; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.function.Function; import org.springframework.context.MessageSource; @@ -97,20 +99,41 @@ public String getMessage() { @Override public Object[] getDetailMessageArguments() { - return new Object[] { - errorsToStringList(getBindingResult().getGlobalErrors()), - errorsToStringList(getBindingResult().getFieldErrors()) - }; + return new Object[] {errorsToStringList(getGlobalErrors()), errorsToStringList(getFieldErrors())}; } @Override public Object[] getDetailMessageArguments(MessageSource messageSource, Locale locale) { return new Object[] { - errorsToStringList(getBindingResult().getGlobalErrors(), messageSource, locale), - errorsToStringList(getBindingResult().getFieldErrors(), messageSource, locale) + errorsToStringList(getGlobalErrors(), messageSource, locale), + errorsToStringList(getFieldErrors(), messageSource, locale) }; } + /** + * Resolve global and field errors to messages with the given + * {@link MessageSource} and {@link Locale}. + * @return a Map with errors as key and resolves messages as value + * @since 6.0.3 + */ + public Map resolveErrorMessages(MessageSource messageSource, Locale locale) { + Map map = new LinkedHashMap<>(); + addMessages(map, getGlobalErrors(), messageSource, locale); + addMessages(map, getFieldErrors(), messageSource, locale); + return map; + } + + private static void addMessages( + Map map, List errors, + MessageSource messageSource, Locale locale) { + + List messages = errorsToStringList(errors, messageSource, locale); + for (int i = 0; i < errors.size(); i++) { + map.put(errors.get(i), messages.get(i)); + } + } + + /** * Convert each given {@link ObjectError} to a String in single quotes, taking * either the error's default message, or its error code. diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java index 444266853515..1c1adcac7257 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java @@ -17,6 +17,7 @@ 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; @@ -292,8 +293,8 @@ public String getMessage() { StringBuilder sb = new StringBuilder("Validation failed for argument at index ") .append(parameter.getParameterIndex()).append(" in method: ") .append(parameter.getExecutable().toGenericString()) - .append(", with ").append(this.bindingResult.getErrorCount()).append(" error(s): "); - for (ObjectError error : this.bindingResult.getAllErrors()) { + .append(", with ").append(getErrorCount()).append(" error(s): "); + for (ObjectError error : getAllErrors()) { sb.append('[').append(error).append("] "); } return sb.toString(); @@ -302,11 +303,35 @@ public String getMessage() { @Override public Object[] getDetailMessageArguments(MessageSource source, Locale locale) { return new Object[] { - MethodArgumentNotValidException.errorsToStringList(this.bindingResult.getGlobalErrors(), source, locale), - MethodArgumentNotValidException.errorsToStringList(this.bindingResult.getFieldErrors(), source, locale) + MethodArgumentNotValidException.errorsToStringList(getGlobalErrors(), source, locale), + MethodArgumentNotValidException.errorsToStringList(getFieldErrors(), source, locale) }; } + /** + * Resolve global and field errors to messages with the given + * {@link MessageSource} and {@link Locale}. + * @return a Map with errors as key and resolves messages as value + * @since 6.0.3 + */ + public Map resolveErrorMessages(MessageSource messageSource, Locale locale) { + Map map = new LinkedHashMap<>(); + addMessages(map, getGlobalErrors(), messageSource, locale); + addMessages(map, getFieldErrors(), messageSource, locale); + return map; + } + + private static void addMessages( + Map map, List errors, + MessageSource messageSource, Locale locale) { + + List messages = MethodArgumentNotValidException.errorsToStringList(errors, messageSource, locale); + for (int i = 0; i < errors.size(); i++) { + map.put(errors.get(i), messages.get(i)); + } + } + + @Override public boolean equals(@Nullable Object other) { return (this == other || this.bindingResult.equals(other)); diff --git a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java index f97e3e5d73ac..cbf9cfb71102 100644 --- a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java +++ b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java @@ -20,10 +20,13 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.function.BiFunction; import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.MessageSource; import org.springframework.context.support.StaticMessageSource; import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; @@ -35,6 +38,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingMatrixVariableException; import org.springframework.web.bind.MissingPathVariableException; @@ -243,11 +247,12 @@ void methodArgumentNotValidException() { MessageSourceTestHelper messageSourceHelper = new MessageSourceTestHelper(MethodArgumentNotValidException.class); BindingResult bindingResult = messageSourceHelper.initBindingResult(); - ErrorResponse ex = new MethodArgumentNotValidException(this.methodParameter, bindingResult); + MethodArgumentNotValidException ex = new MethodArgumentNotValidException(this.methodParameter, bindingResult); assertStatus(ex, HttpStatus.BAD_REQUEST); assertDetail(ex, "Invalid request content."); messageSourceHelper.assertDetailMessage(ex); + messageSourceHelper.assertErrorMessages(ex::resolveErrorMessages); assertThat(ex.getHeaders()).isEmpty(); } @@ -361,6 +366,7 @@ void webExchangeBindException() { assertStatus(ex, HttpStatus.BAD_REQUEST); assertDetail(ex, "Invalid request content."); messageSourceHelper.assertDetailMessage(ex); + messageSourceHelper.assertErrorMessages(ex::resolveErrorMessages); assertThat(ex.getHeaders()).isEmpty(); } @@ -444,12 +450,8 @@ public BindingResult initBindingResult() { } private void assertDetailMessage(ErrorResponse ex) { - StaticMessageSource messageSource = new StaticMessageSource(); - messageSource.addMessage(this.code, Locale.UK, "Failures {0}. nested failures: {1}"); - messageSource.addMessage("bean.invalid.A", Locale.UK, "Bean A message"); - messageSource.addMessage("bean.invalid.B", Locale.UK, "Bean B message"); - messageSource.addMessage("name.required", Locale.UK, "Required name message"); - messageSource.addMessage("age.min", Locale.UK, "Minimum age message"); + + StaticMessageSource messageSource = initMessageSource(); String message = messageSource.getMessage( ex.getDetailMessageCode(), ex.getDetailMessageArguments(), Locale.UK); @@ -465,6 +467,24 @@ private void assertDetailMessage(ErrorResponse ex) { "Failures ['Bean A message', 'Bean B message']. " + "nested failures: [name: 'Required name message', age: 'Minimum age message']"); } + + private void assertErrorMessages(BiFunction> expectedMessages) { + StaticMessageSource messageSource = initMessageSource(); + Map map = expectedMessages.apply(messageSource, Locale.UK); + + assertThat(map).hasSize(4).containsValues( + "'Bean A message'", "'Bean B message'", "name: 'Required name message'", "age: 'Minimum age message'"); + } + + private StaticMessageSource initMessageSource() { + StaticMessageSource messageSource = new StaticMessageSource(); + messageSource.addMessage(this.code, Locale.UK, "Failures {0}. nested failures: {1}"); + messageSource.addMessage("bean.invalid.A", Locale.UK, "Bean A message"); + messageSource.addMessage("bean.invalid.B", Locale.UK, "Bean B message"); + messageSource.addMessage("name.required", Locale.UK, "Required name message"); + messageSource.addMessage("age.min", Locale.UK, "Minimum age message"); + return messageSource; + } } }