From eeb4d55b4ca9c68d8722ded891f72428c4235103 Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 30 Nov 2024 00:49:23 +0900 Subject: [PATCH] Wrap MethodValidationResult with ErrorWrapper Signed-off-by: yongjunhong --- .../boot/web/error/ErrorWrapper.java | 90 +++++++++++++++++++ .../error/DefaultErrorAttributes.java | 12 +-- .../servlet/error/DefaultErrorAttributes.java | 27 +++--- .../error/DefaultErrorAttributesTests.java | 25 ++++++ 4 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java new file mode 100644 index 000000000000..e0d7e9d021b8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2024 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.boot.web.error; + +import jakarta.annotation.Nullable; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.util.Assert; + +/** + * A wrapper class for error objects that implements {@link MessageSourceResolvable}. + * This class extends {@link DefaultMessageSourceResolvable} and delegates the + * message resolution to the wrapped error object. + * + * @author Yongjun Hong + * @since 3.5.0 + */ +public class ErrorWrapper extends DefaultMessageSourceResolvable { + + private final Object error; + + /** + * Create a new {@code ErrorWrapper} instance with the specified error. + * + * @param error the error object to wrap (must not be {@code null}) + */ + public ErrorWrapper(Object error) { + this(error, null, null, null); + } + + /** + * Create a new {@code ErrorWrapper} instance with the specified error, codes, + * arguments, and default message. + * + * @param error the error object to wrap (must not be {@code null}) + * @param codes the codes to be used for message resolution + * @param arguments the arguments to be used for message resolution + * @param defaultMessage the default message to be used if no message is found + */ + public ErrorWrapper(Object error, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) { + super(codes, arguments, defaultMessage); + Assert.notNull(error, "Error must not be null"); + this.error = error; + } + + /** + * Return the codes to be used for message resolution. + * + * @return the codes to be used for message resolution + */ + @Override + public String[] getCodes() { + return ((MessageSourceResolvable) this.error).getCodes(); + } + + /** + * Return the arguments to be used for message resolution. + * + * @return the arguments to be used for message resolution + */ + @Override + public Object[] getArguments() { + return ((MessageSourceResolvable) this.error).getArguments(); + } + + /** + * Return the default message to be used if no message is found. + * + * @return the default message to be used if no message is found + */ + @Override + public String getDefaultMessage() { + return ((MessageSourceResolvable) this.error).getDefaultMessage(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java index c02b439fa9d5..db8bc3f8082a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Optional; +import org.springframework.boot.web.error.ErrorWrapper; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.core.annotation.MergedAnnotation; @@ -32,7 +33,6 @@ import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; import org.springframework.validation.method.MethodValidationResult; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.reactive.function.server.ServerRequest; @@ -48,8 +48,8 @@ *
  • error - The error reason
  • *
  • exception - The class name of the root exception (if configured)
  • *
  • message - The exception message (if configured)
  • - *
  • errors - Any {@link ObjectError}s from a {@link BindingResult} or - * {@link MethodValidationResult} exception (if configured)
  • + *
  • errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a + * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
  • *
  • trace - The exception stack trace (if configured)
  • *
  • path - The URL path when the exception was raised
  • *
  • requestId - Unique ID associated with the current request
  • @@ -61,6 +61,7 @@ * @author Scott Frederick * @author Moritz Halbritter * @author Yanming Zhou + * @author Yongjun Hong * @since 2.0.0 * @see ErrorAttributes */ @@ -141,10 +142,9 @@ else if (error instanceof ResponseStatusException responseStatusException) { private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, MethodValidationResult result) { - List errors = result.getAllErrors() + List errors = result.getAllErrors() .stream() - .filter(ObjectError.class::isInstance) - .map(ObjectError.class::cast) + .map(ErrorWrapper::new) .toList(); errorAttributes.put("message", "Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java index 9c351d633f79..fdeceb6ee62e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java @@ -30,6 +30,7 @@ import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; +import org.springframework.boot.web.error.ErrorWrapper; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; @@ -52,8 +53,8 @@ *
  • error - The error reason
  • *
  • exception - The class name of the root exception (if configured)
  • *
  • message - The exception message (if configured)
  • - *
  • errors - Any {@link ObjectError}s from a {@link BindingResult} or - * {@link MethodValidationResult} exception (if configured)
  • + *
  • errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a + * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
  • *
  • trace - The exception stack trace (if configured)
  • *
  • path - The URL path when the exception was raised
  • * @@ -65,6 +66,7 @@ * @author Scott Frederick * @author Moritz Halbritter * @author Yanming Zhou + * @author Yongjun Hong * @since 2.0.0 * @see ErrorAttributes */ @@ -153,6 +155,17 @@ private void addErrorMessage(Map errorAttributes, WebRequest web } } + private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, + MethodValidationResult result) { + List errors = result.getAllErrors() + .stream() + .map(ErrorWrapper::new) + .toList(); + errorAttributes.put("message", + "Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size()); + errorAttributes.put("errors", errors); + } + private void addExceptionErrorMessage(Map errorAttributes, WebRequest webRequest, Throwable error) { errorAttributes.put("message", getMessage(webRequest, error)); } @@ -187,16 +200,6 @@ private void addMessageAndErrorsFromBindingResult(Map errorAttri result.getAllErrors()); } - private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, - MethodValidationResult result) { - List errors = result.getAllErrors() - .stream() - .filter(ObjectError.class::isInstance) - .map(ObjectError.class::cast) - .toList(); - addMessageAndErrorsForValidationFailure(errorAttributes, "method='" + result.getMethod() + "'", errors); - } - private void addMessageAndErrorsForValidationFailure(Map errorAttributes, String validated, List errors) { errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size()); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java index f24de2fe8e5e..5ac81ae153c7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java @@ -55,6 +55,7 @@ * @author Scott Frederick * @author Moritz Halbritter * @author Yanming Zhou + * @author Yongjun Hong */ class DefaultErrorAttributesTests { @@ -326,6 +327,30 @@ void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception { assertThat(attributes).doesNotContainKey("errors"); } + @Test + void extractParameterValidationResultErrors() throws Exception { + Object target = "test"; + Method method = String.class.getMethod("substring", int.class); + MethodParameter parameter = new MethodParameter(method, 0); + ParameterValidationResult parameterValidationResult = new ParameterValidationResult(parameter, -1, + List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null, + (error, sourceType) -> { + throw new IllegalArgumentException("No source object of the given type"); + }); + MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method, + List.of(parameterValidationResult)); + HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult); + MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); + + Map attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex), + ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS)); + + assertThat(attributes.get("message")).asString() + .isEqualTo("Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1"); + assertThat(attributes).containsEntry("errors", + methodValidationResult.getAllErrors().stream().filter(ObjectError.class::isInstance).toList()); + } + @Test void excludeStatus() { ResponseStatusException error = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE,