From d5dab3836e55205f42764c3eb36246aea1bbdab4 Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Wed, 18 Sep 2024 21:19:02 +0200 Subject: [PATCH 1/7] docs: Replace deprecated methods of Spring Security with new alternatives Fixes #100 --- src/docs/asciidoc/index.adoc | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 6286452..e4fa213 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1212,8 +1212,10 @@ import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageM import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper; import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; public class WebSecurityConfiguration { @Bean @@ -1227,11 +1229,11 @@ public class WebSecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, UnauthorizedEntryPoint unauthorizedEntryPoint) throws Exception { - http.httpBasic().disable(); + http.httpBasic(AbstractHttpConfigurer::disable); - http.authorizeHttpRequests().anyRequest().authenticated(); + http.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated()); - http.exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint);//<.> + http.exceptionHandling(customizer -> customizer.authenticationEntryPoint(unauthorizedEntryPoint));//<.> return http.build(); } @@ -1258,6 +1260,7 @@ import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMap import org.springframework.context.annotation.Bean; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; public class WebSecurityConfiguration { @@ -1272,11 +1275,11 @@ public class WebSecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, AccessDeniedHandler accessDeniedHandler) throws Exception { - http.httpBasic().disable(); + http.httpBasic(AbstractHttpConfigurer::disable); - http.authorizeHttpRequests().anyRequest().authenticated(); + http.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated()); - http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);//<.> + http.exceptionHandling(customizer -> customizer.accessDeniedHandler(accessDeniedHandler));//<.> return http.build(); } From ccb5bc5ca9b1107e51031cadf7db2b369b9229e0 Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Wed, 18 Sep 2024 21:21:41 +0200 Subject: [PATCH 2/7] test: Replace deprecated methods of Spring Security with new alternatives Fixes #100 --- .../SpringSecurityApiExceptionHandlerTest.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/SpringSecurityApiExceptionHandlerTest.java b/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/SpringSecurityApiExceptionHandlerTest.java index 62f9db3..2613b46 100644 --- a/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/SpringSecurityApiExceptionHandlerTest.java +++ b/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/SpringSecurityApiExceptionHandlerTest.java @@ -17,6 +17,7 @@ import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; @@ -130,15 +131,15 @@ public AccessDeniedHandler accessDeniedHandler(HttpStatusMapper httpStatusMapper public SecurityFilterChain securityFilterChain(HttpSecurity http, UnauthorizedEntryPoint unauthorizedEntryPoint, AccessDeniedHandler accessDeniedHandler) throws Exception { - http.httpBasic().disable(); + http.httpBasic(AbstractHttpConfigurer::disable); - http.authorizeHttpRequests() - .requestMatchers("/test/spring-security/admin-global").hasRole("ADMIN") - .anyRequest().authenticated(); + http.authorizeHttpRequests(customizer -> customizer + .requestMatchers("/test/spring-security/admin-global").hasRole("ADMIN") + .anyRequest().authenticated()); - http.exceptionHandling() - .authenticationEntryPoint(unauthorizedEntryPoint) - .accessDeniedHandler(accessDeniedHandler); + http.exceptionHandling(customizer -> customizer + .authenticationEntryPoint(unauthorizedEntryPoint) + .accessDeniedHandler(accessDeniedHandler)); return http.build(); } From 88ad86e5ea3131b742deb468f5e557d58097c22e Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Fri, 20 Sep 2024 09:21:57 +0200 Subject: [PATCH 3/7] feat: Allow override of the `message` property for a violation exception Fixes #103 --- .../ConstraintViolationApiExceptionHandler.java | 10 +++++----- .../mapper/ErrorMessageMapper.java | 6 ++++++ ...ConstraintViolationApiExceptionHandlerTest.java | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandler.java b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandler.java index 1960ab9..cd7c048 100644 --- a/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandler.java +++ b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandler.java @@ -4,14 +4,13 @@ import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper; import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper; import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; - import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.ElementKind; import jakarta.validation.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import java.util.Comparator; import java.util.Optional; @@ -136,6 +135,7 @@ private String getMessage(ConstraintViolation constraintViolation) { } private String getMessage(ConstraintViolationException exception) { - return "Validation failed. Error count: " + exception.getConstraintViolations().size(); + return errorMessageMapper.getErrorMessageIfConfiguredInProperties(exception) + .orElseGet(() -> "Validation failed. Error count: " + exception.getConstraintViolations().size()); } } diff --git a/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/mapper/ErrorMessageMapper.java b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/mapper/ErrorMessageMapper.java index f87c7d3..98e5d3d 100644 --- a/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/mapper/ErrorMessageMapper.java +++ b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/mapper/ErrorMessageMapper.java @@ -2,6 +2,8 @@ import io.github.wimdeblauwe.errorhandlingspringbootstarter.ErrorHandlingProperties; +import java.util.Optional; + import static org.springframework.util.StringUtils.hasText; /** @@ -22,6 +24,10 @@ public String getErrorMessage(Throwable exception) { return exception.getMessage(); } + public Optional getErrorMessageIfConfiguredInProperties(Throwable exception) { + return Optional.ofNullable(getErrorMessageFromProperties(exception.getClass())); + } + public String getErrorMessage(String fieldSpecificCode, String code, String defaultMessage) { if (properties.getMessages().containsKey(fieldSpecificCode)) { return properties.getMessages().get(fieldSpecificCode); diff --git a/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandlerTest.java b/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandlerTest.java index 7773ec3..b2d95f0 100644 --- a/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandlerTest.java +++ b/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/ConstraintViolationApiExceptionHandlerTest.java @@ -110,6 +110,20 @@ void testErrorCodeOverride(@Autowired ErrorHandlingProperties properties) throws ; } + @Test + @WithMockUser + void testErrorMessageOverride(@Autowired ErrorHandlingProperties properties) throws Exception { + properties.getMessages().put("jakarta.validation.ConstraintViolationException", "There was a validation failure."); + mockMvc.perform(post("/test/validation") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"value2\": \"\"}") + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("code").value("VALIDATION_FAILED")) + .andExpect(jsonPath("message").value("There was a validation failure.")) + ; + } + @Test @WithMockUser void testFieldErrorCodeOverride(@Autowired ErrorHandlingProperties properties) throws Exception { From 7236fc6e07f135dd5e879303cda8c9827a6a48b8 Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Tue, 24 Sep 2024 22:06:52 +0200 Subject: [PATCH 4/7] docs: Add documentation on how to override the message of an exception Fixes #103 --- src/docs/asciidoc/index.adoc | 54 ++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index e4fa213..5d7c4cc 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -472,8 +472,58 @@ The response JSON: ==== General override of error messages -By using `error.handling.messages` property, it is possible to globally set an error message for a certain exception. -This is most useful for the validation messages. +By using `error.handling.messages` property, it is possible to globally set an error message for a certain exception or a certain validation annotation. + +===== Exception + +Suppose you have this defined: + +[source,properties] +---- +error.handling.messages.com.company.application.user.UserNotFoundException=The user was not found +---- + +The response JSON: + +[source,json] +---- +{ + "code": "USER_NOT_FOUND", + "message": "The user was not found" //<.> +} +---- + +<.> The output uses the configured override. + +This can also be used for exception types that are not part of your own application. + +For example: + +[source,properties] +---- +error.handling.messages.jakarta.validation.ConstraintViolationException=There was a validation failure. +---- + +Will output the following JSON: + +[source,json] +---- +{ + "code": "VALIDATION_FAILED", + "message": "There was a validation failure.", + "fieldErrors": [ + { + "code": "INVALID_SIZE", + "property": "name", + "message": "size must be between 10 and 2147483647", + "rejectedValue": "", + "path": "name" + } + ] +} +---- + +===== Validation annotation Suppose you have this defined: From c6dcae3e2d2c087f10c424786bea1a517962257c Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Tue, 24 Sep 2024 22:08:59 +0200 Subject: [PATCH 5/7] Set version to 4.5.0-SNAPSHOT for next release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 316a967..3bae339 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ io.github.wimdeblauwe error-handling-spring-boot-starter - 4.4.0 + 4.5.0-SNAPSHOT Error Handling Spring Boot Starter Spring Boot starter that configures error handling From 5c8f8efacf98ff5b886211ee8b9769499f7e1473 Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Tue, 24 Sep 2024 22:58:41 +0200 Subject: [PATCH 6/7] feat: Support HandlerMethodValidationException Fixes #102 --- .../AbstractErrorHandlingConfiguration.java | 9 + ...ndlerMethodValidationExceptionHandler.java | 60 +++++++ .../error-handling-defaults.properties | 2 + ...rMethodValidationExceptionHandlerTest.java | 154 ++++++++++++++++++ 4 files changed, 225 insertions(+) create mode 100644 src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandler.java create mode 100644 src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandlerTest.java diff --git a/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/AbstractErrorHandlingConfiguration.java b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/AbstractErrorHandlingConfiguration.java index 04b4833..a043026 100644 --- a/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/AbstractErrorHandlingConfiguration.java +++ b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/AbstractErrorHandlingConfiguration.java @@ -1,6 +1,7 @@ package io.github.wimdeblauwe.errorhandlingspringbootstarter; import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.BindApiExceptionHandler; +import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.HandlerMethodValidationExceptionHandler; import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.HttpMessageNotReadableApiExceptionHandler; import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.TypeMismatchApiExceptionHandler; import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper; @@ -91,6 +92,14 @@ public BindApiExceptionHandler bindApiExceptionHandler(ErrorHandlingProperties p return new BindApiExceptionHandler(properties, httpStatusMapper, errorCodeMapper, errorMessageMapper); } + @Bean + @ConditionalOnMissingBean + public HandlerMethodValidationExceptionHandler handlerMethodValidationExceptionHandler(HttpStatusMapper httpStatusMapper, + ErrorCodeMapper errorCodeMapper, + ErrorMessageMapper errorMessageMapper) { + return new HandlerMethodValidationExceptionHandler(httpStatusMapper, errorCodeMapper, errorMessageMapper); + } + @Bean @ConditionalOnMissingBean public ApiErrorResponseSerializer apiErrorResponseSerializer(ErrorHandlingProperties properties) { diff --git a/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandler.java b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandler.java new file mode 100644 index 0000000..6ee22ba --- /dev/null +++ b/src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandler.java @@ -0,0 +1,60 @@ +package io.github.wimdeblauwe.errorhandlingspringbootstarter.handler; + +import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponse; +import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiFieldError; +import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiGlobalError; +import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper; +import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper; +import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.web.method.annotation.HandlerMethodValidationException; + +import java.util.List; +import java.util.Optional; + +public class HandlerMethodValidationExceptionHandler extends AbstractApiExceptionHandler { + + public HandlerMethodValidationExceptionHandler(HttpStatusMapper httpStatusMapper, + ErrorCodeMapper errorCodeMapper, + ErrorMessageMapper errorMessageMapper) { + + super(httpStatusMapper, errorCodeMapper, errorMessageMapper); + } + + @Override + public boolean canHandle(Throwable exception) { + return exception instanceof HandlerMethodValidationException; + } + + @Override + public ApiErrorResponse handle(Throwable ex) { + var response = new ApiErrorResponse(HttpStatus.BAD_REQUEST, getErrorCode(ex), getErrorMessage(ex)); + var validationException = (HandlerMethodValidationException) ex; + List errors = validationException.getAllErrors(); + + errors.forEach(error -> { + if (error instanceof FieldError fieldError) { + var apiFieldError = new ApiFieldError( + errorCodeMapper.getErrorCode(fieldError.getCode()), + fieldError.getField(), + errorMessageMapper.getErrorMessage(fieldError.getCode(), fieldError.getDefaultMessage()), + fieldError.getRejectedValue(), + null); + response.addFieldError(apiFieldError); + } else { + var lastCode = Optional.ofNullable(error.getCodes()) + .filter(codes -> codes.length > 0) + .map(codes -> codes[codes.length - 1]) + .orElse(null); + var apiGlobalErrorMessage = new ApiGlobalError( + errorCodeMapper.getErrorCode(lastCode), + errorMessageMapper.getErrorMessage(lastCode, error.getDefaultMessage())); + response.addGlobalError(apiGlobalErrorMessage); + } + }); + + return response; + } +} diff --git a/src/main/resources/error-handling-defaults.properties b/src/main/resources/error-handling-defaults.properties index 2f5fa11..e911ede 100644 --- a/src/main/resources/error-handling-defaults.properties +++ b/src/main/resources/error-handling-defaults.properties @@ -1,4 +1,6 @@ error.handling.codes.org.springframework.web.bind.MethodArgumentNotValidException=VALIDATION_FAILED +error.handling.codes.org.springframework.web.method.annotation.HandlerMethodValidationException=VALIDATION_FAILED +error.handling.messages.org.springframework.web.method.annotation.HandlerMethodValidationException=There was a validation failure. error.handling.codes.org.springframework.http.converter.HttpMessageNotReadableException=MESSAGE_NOT_READABLE error.handling.codes.jakarta.validation.ConstraintViolationException=VALIDATION_FAILED error.handling.codes.org.springframework.beans.TypeMismatchException=TYPE_MISMATCH diff --git a/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandlerTest.java b/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandlerTest.java new file mode 100644 index 0000000..48cc96f --- /dev/null +++ b/src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/handler/HandlerMethodValidationExceptionHandlerTest.java @@ -0,0 +1,154 @@ +package io.github.wimdeblauwe.errorhandlingspringbootstarter.handler; + +import io.github.wimdeblauwe.errorhandlingspringbootstarter.ErrorHandlingProperties; +import io.github.wimdeblauwe.errorhandlingspringbootstarter.servlet.ServletErrorHandlingConfiguration; +import jakarta.validation.*; +import jakarta.validation.constraints.NotNull; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockPart; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.lang.annotation.*; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest +@ContextConfiguration(classes = {ServletErrorHandlingConfiguration.class, + HandlerMethodValidationExceptionHandlerTest.TestController.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class HandlerMethodValidationExceptionHandlerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @WithMockUser + void testHandlerMethodViolationException() throws Exception { + mockMvc.perform(multipart("/test/update-event") + .part(new MockPart("eventRequest", null, "{}".getBytes(StandardCharsets.UTF_8), MediaType.APPLICATION_JSON)) + .part(new MockPart("file", "file.jpg", new byte[0], MediaType.IMAGE_JPEG)) + .with(request -> { + request.setMethod(HttpMethod.PUT.name()); + return request; + }) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("code").value("VALIDATION_FAILED")) + .andExpect(jsonPath("message").value("There was a validation failure.")) + .andExpect(jsonPath("fieldErrors", hasSize(1))) + .andExpect(jsonPath("fieldErrors..code", allOf(hasItem("REQUIRED_NOT_NULL")))) + .andExpect(jsonPath("fieldErrors..property", allOf(hasItem("dateTime")))) + .andExpect(jsonPath("fieldErrors..message", allOf(hasItem("must not be null")))) + .andExpect(jsonPath("fieldErrors..rejectedValue", allOf(hasItem(nullValue())))) + .andExpect(jsonPath("globalErrors", hasSize(1))) + .andExpect(jsonPath("globalErrors..code", allOf(hasItem("ValidFileType")))) + .andExpect(jsonPath("globalErrors..message", allOf(hasItem("")))) + ; + } + + @Test + @WithMockUser + void testHandlerMethodViolationException_customValidationAnnotationOverride(@Autowired ErrorHandlingProperties properties) throws Exception { + properties.getCodes().put("ValidFileType", "INVALID_FILE_TYPE"); + properties.getMessages().put("ValidFileType", "The file type is invalid. Only text/plain and application/pdf allowed."); + mockMvc.perform(multipart("/test/update-event") + .part(new MockPart("eventRequest", null, "{}".getBytes(StandardCharsets.UTF_8), MediaType.APPLICATION_JSON)) + .part(new MockPart("file", "file.jpg", new byte[0], MediaType.IMAGE_JPEG)) + .with(request -> { + request.setMethod(HttpMethod.PUT.name()); + return request; + }) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("code").value("VALIDATION_FAILED")) + .andExpect(jsonPath("message").value("There was a validation failure.")) + .andExpect(jsonPath("fieldErrors", hasSize(1))) + .andExpect(jsonPath("fieldErrors..code", allOf(hasItem("REQUIRED_NOT_NULL")))) + .andExpect(jsonPath("fieldErrors..property", allOf(hasItem("dateTime")))) + .andExpect(jsonPath("fieldErrors..message", allOf(hasItem("must not be null")))) + .andExpect(jsonPath("fieldErrors..rejectedValue", allOf(hasItem(nullValue())))) + .andExpect(jsonPath("globalErrors", hasSize(1))) + .andExpect(jsonPath("globalErrors..code", allOf(hasItem("INVALID_FILE_TYPE")))) + .andExpect(jsonPath("globalErrors..message", allOf(hasItem("The file type is invalid. Only text/plain and application/pdf allowed.")))) + ; + } + + @RestController + @RequestMapping + static class TestController { + + @PutMapping("/test/update-event") + public void updateEvent( + @Valid @RequestPart EventRequest eventRequest, + @Valid @ValidFileType @RequestPart MultipartFile file) { + + } + } + + static class EventRequest { + @NotNull + private LocalDateTime dateTime; + + public LocalDateTime getDateTime() { + return dateTime; + } + + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + } + + @Documented + @Constraint(validatedBy = MultiPartFileValidator.class) + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @interface ValidFileType { + + // Default list of allowed file types + String[] value() default { + MediaType.TEXT_PLAIN_VALUE, + MediaType.APPLICATION_PDF_VALUE + }; + + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + static class MultiPartFileValidator implements ConstraintValidator { + + private List allowed; + + @Override + public void initialize(ValidFileType constraintAnnotation) { + allowed = List.of(constraintAnnotation.value()); + } + + @Override + public boolean isValid(MultipartFile file, ConstraintValidatorContext context) { + return file == null || allowed.contains(file.getContentType()); + } + } +} From c053c4497b630693773a25a969fdf261a7d0c242 Mon Sep 17 00:00:00 2001 From: Wim Deblauwe Date: Wed, 25 Sep 2024 08:25:03 +0200 Subject: [PATCH 7/7] Release 4.5.0 --- README.adoc | 5 +++++ pom.xml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 119ba62..1b2d8de 100644 --- a/README.adoc +++ b/README.adoc @@ -33,6 +33,11 @@ NOTE: Documentation is very important to us, so if you find something missing fr |=== |error-handling-spring-boot-starter |Spring Boot|Minimum Java version|Docs +|https://github.com/wimdeblauwe/error-handling-spring-boot-starter/releases/tag/4.5.0[4.5.0] +|3.3.x +|17 +|https://wimdeblauwe.github.io/error-handling-spring-boot-starter/4.5.0/[Documentation 4.5.0] + |https://github.com/wimdeblauwe/error-handling-spring-boot-starter/releases/tag/4.4.0[4.4.0] |3.3.x |17 diff --git a/pom.xml b/pom.xml index 3bae339..d75c5d1 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ io.github.wimdeblauwe error-handling-spring-boot-starter - 4.5.0-SNAPSHOT + 4.5.0 Error Handling Spring Boot Starter Spring Boot starter that configures error handling