diff --git a/spring-exceptions/pom.xml b/spring-exceptions/pom.xml index cf5d2b1c60d5..b5e72d536c63 100644 --- a/spring-exceptions/pom.xml +++ b/spring-exceptions/pom.xml @@ -133,6 +133,35 @@ jaxb-api ${jaxb-api.version} + + org.projectlombok + lombok + 1.18.32 + provided + + + org.assertj + assertj-core + 3.24.2 + test + + + org.glassfish.expressly + expressly + 5.0.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + test + + + com.jayway.jsonpath + json-path + 2.8.0 + test + @@ -179,4 +208,4 @@ 1.6.1 - \ No newline at end of file + diff --git a/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/GlobalExceptionHandler.java b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/GlobalExceptionHandler.java new file mode 100644 index 000000000000..f1f192a96203 --- /dev/null +++ b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/GlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package com.baeldung.bindcustomvalidationmessage; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationErrors( + MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + + ex.getBindingResult() + .getFieldErrors() + .forEach(error -> errors.put(error.getField(), error.getDefaultMessage())); + + return ResponseEntity.badRequest().body(errors); + } +} diff --git a/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/UserController.java b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/UserController.java new file mode 100644 index 000000000000..a47e4f5e30ed --- /dev/null +++ b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/UserController.java @@ -0,0 +1,18 @@ +package com.baeldung.bindcustomvalidationmessage; + +import com.baeldung.bindcustomvalidationmessage.externalfilevalidation.UserDTO; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +public class UserController { + @PostMapping + public ResponseEntity createUser(@Valid @RequestBody UserDTO user) { + return ResponseEntity.ok("User created successfully"); + } +} diff --git a/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/PasswordDTO.java b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/PasswordDTO.java new file mode 100644 index 000000000000..6b629cf82f35 --- /dev/null +++ b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/PasswordDTO.java @@ -0,0 +1,11 @@ +package com.baeldung.bindcustomvalidationmessage.customvalidator; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PasswordDTO { + @ValidPassword + private String password; +} diff --git a/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/PasswordValidator.java b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/PasswordValidator.java new file mode 100644 index 000000000000..42fc90f26edb --- /dev/null +++ b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/PasswordValidator.java @@ -0,0 +1,11 @@ +package com.baeldung.bindcustomvalidationmessage.customvalidator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class PasswordValidator implements ConstraintValidator { + @Override + public boolean isValid(String password, ConstraintValidatorContext context) { + return password != null && password.length() >= 8 && password.matches(".*\\d.*"); + } +} diff --git a/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/ValidPassword.java b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/ValidPassword.java new file mode 100644 index 000000000000..be6f399dbe1c --- /dev/null +++ b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/customvalidator/ValidPassword.java @@ -0,0 +1,18 @@ +package com.baeldung.bindcustomvalidationmessage.customvalidator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PasswordValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidPassword { + String message() default "{user.password.invalid}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/externalfilevalidation/UserDTO.java b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/externalfilevalidation/UserDTO.java new file mode 100644 index 000000000000..0253ea46f3ea --- /dev/null +++ b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/externalfilevalidation/UserDTO.java @@ -0,0 +1,17 @@ +package com.baeldung.bindcustomvalidationmessage.externalfilevalidation; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class UserDTO { + @NotNull(message = "{user.name.notnull}") + @Size(min = 2, message = "{user.name.size}") + private String name; + + @NotNull(message = "{user.email.notnull}") + @Email(message = "{user.email.invalid}") + private String email; +} diff --git a/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/inlinevalidation/UserDTO.java b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/inlinevalidation/UserDTO.java new file mode 100644 index 000000000000..cb364c11f943 --- /dev/null +++ b/spring-exceptions/src/main/java/com/baeldung/bindcustomvalidationmessage/inlinevalidation/UserDTO.java @@ -0,0 +1,17 @@ +package com.baeldung.bindcustomvalidationmessage.inlinevalidation; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class UserDTO { + @NotNull(message = "User name must not be null") + @Size(min = 2, message = "User name must be at least 2 characters long") + private String name; + + @NotNull(message = "Email is required") + @Email(message = "Please provide a valid email") + private String email; +} diff --git a/spring-exceptions/src/main/resources/ValidationMessages.properties b/spring-exceptions/src/main/resources/ValidationMessages.properties new file mode 100644 index 000000000000..93359966a672 --- /dev/null +++ b/spring-exceptions/src/main/resources/ValidationMessages.properties @@ -0,0 +1,6 @@ +user.name.notnull=User name must not be null +user.name.size=User name must be at least 2 characters long +user.email.notnull=Email is required +user.email.invalid=Please provide a valid email +user.password.invalid=Password must be at least 8 characters and contain a number + diff --git a/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/customvalidation/ValidPasswordValidatorTest.java b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/customvalidation/ValidPasswordValidatorTest.java new file mode 100644 index 000000000000..e64967c4590a --- /dev/null +++ b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/customvalidation/ValidPasswordValidatorTest.java @@ -0,0 +1,49 @@ +package com.baeldung.bindcustomvalidationmessage.customvalidation; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.baeldung.bindcustomvalidationmessage.customvalidator.PasswordDTO; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ValidPasswordValidatorTest { + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void whenPasswordIsValid_thenNoViolations() { + PasswordDTO dto = new PasswordDTO("Str0ngP@ssword123"); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + void whenPasswordLacksDigitsOrSpecials_thenValidationFails() { + PasswordDTO dto = new PasswordDTO("password"); + + Set> violations = validator.validate(dto); + + assertThat(violations).hasSize(1); + } + + @Test + void whenPasswordIsNull_thenValidationFails() { + PasswordDTO dto = new PasswordDTO(null); + + Set> violations = validator.validate(dto); + + assertThat(violations).hasSize(1); + } +} diff --git a/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/externalfilevalidation/UserDTOExtValidationTest.java b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/externalfilevalidation/UserDTOExtValidationTest.java new file mode 100644 index 000000000000..54e208d2088c --- /dev/null +++ b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/externalfilevalidation/UserDTOExtValidationTest.java @@ -0,0 +1,87 @@ +package com.baeldung.bindcustomvalidationmessage.externalfilevalidation; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UserDTOExtValidationTest { + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void whenAllFieldsValid_thenNoValidationErrors() { + UserDTO user = new UserDTO(); + user.setName("John"); + user.setEmail("john@example.com"); + + Set> violations = validator.validate(user); + assertThat(violations).isEmpty(); + } + + @Test + void whenNameIsNull_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName(null); + user.setEmail("john@example.com"); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("name"); + assertThat(violation.getMessage()).isEqualTo("User name must not be null"); + } + + @Test + void whenNameTooShort_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName("A"); + user.setEmail("john@example.com"); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("name"); + assertThat(violation.getMessage()).isEqualTo("User name must be at least 2 characters long"); + } + + @Test + void whenEmailIsInvalid_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName("John"); + user.setEmail("invalid-email"); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("email"); + assertThat(violation.getMessage()).isEqualTo("Please provide a valid email"); + } + + @Test + void whenEmailIsNull_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName("John"); + user.setEmail(null); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("email"); + assertThat(violation.getMessage()).isEqualTo("Email is required"); + } +} diff --git a/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/inlinevalidation/UserDTOInlineValidationTest.java b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/inlinevalidation/UserDTOInlineValidationTest.java new file mode 100644 index 000000000000..71222742fcf8 --- /dev/null +++ b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/inlinevalidation/UserDTOInlineValidationTest.java @@ -0,0 +1,87 @@ +package com.baeldung.bindcustomvalidationmessage.inlinevalidation; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UserDTOInlineValidationTest { + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void whenAllFieldsValid_thenNoValidationErrors() { + UserDTO user = new UserDTO(); + user.setName("John"); + user.setEmail("john@example.com"); + + Set> violations = validator.validate(user); + assertThat(violations).isEmpty(); + } + + @Test + void whenNameIsNull_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName(null); + user.setEmail("john@example.com"); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("name"); + assertThat(violation.getMessage()).isEqualTo("User name must not be null"); + } + + @Test + void whenNameTooShort_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName("A"); + user.setEmail("john@example.com"); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("name"); + assertThat(violation.getMessage()).isEqualTo("User name must be at least 2 characters long"); + } + + @Test + void whenEmailIsInvalid_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName("John"); + user.setEmail("invalid-email"); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("email"); + assertThat(violation.getMessage()).isEqualTo("Please provide a valid email"); + } + + @Test + void whenEmailIsNull_thenValidationFails() { + UserDTO user = new UserDTO(); + user.setName("John"); + user.setEmail(null); + + Set> violations = validator.validate(user); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("email"); + assertThat(violation.getMessage()).isEqualTo("Email is required"); + } +} diff --git a/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/integrationtest/UserControllerTest.java b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/integrationtest/UserControllerTest.java new file mode 100644 index 000000000000..0cdd9669533a --- /dev/null +++ b/spring-exceptions/src/test/java/com/baeldung/bindcustomvalidationmessage/integrationtest/UserControllerTest.java @@ -0,0 +1,131 @@ +package com.baeldung.bindcustomvalidationmessage.integrationtest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.baeldung.bindcustomvalidationmessage.GlobalExceptionHandler; +import com.baeldung.bindcustomvalidationmessage.UserController; +import com.baeldung.bindcustomvalidationmessage.externalfilevalidation.UserDTO; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +class UserControllerTest { + private MockMvc mockMvc; + private ObjectMapper objectMapper; + + @BeforeEach + void setup() { + UserController controller = new UserController(); + + LocalValidatorFactoryBean validatorFactory = new LocalValidatorFactoryBean(); + validatorFactory.afterPropertiesSet(); + + this.mockMvc = + MockMvcBuilders.standaloneSetup(controller) + .setValidator(validatorFactory) + .setMessageConverters(new MappingJackson2HttpMessageConverter()) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + this.objectMapper = new ObjectMapper(); + } + + @Test + void whenAllFieldsValid_thenNoValidationErrors() throws Exception { + UserDTO dto = new UserDTO(); + dto.setName("John Doe"); + dto.setEmail("john@example.com"); + + mockMvc + .perform( + post("/users") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().string("\"User created successfully\"")); + } + + @Test + void whenNameIsBlank_thenReturnsValidationError() throws Exception { + UserDTO dto = new UserDTO(); + dto.setName(""); // invalid + dto.setEmail("john@example.com"); + + mockMvc + .perform( + post("/users") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect( + jsonPath("$.name") + .value("User name must be at least 2 characters long")); + } + + @Test + void whenNameIsNull_thenReturnsValidationError() throws Exception { + UserDTO dto = new UserDTO(); + dto.setName(null); // invalid + dto.setEmail("john@example.com"); + + mockMvc + .perform( + post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + void whenNameTooShort_thenReturnsValidationError() throws Exception { + UserDTO dto = new UserDTO(); + dto.setName("A"); // invalid + dto.setEmail("john@example.com"); + + mockMvc + .perform( + post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + void whenEmailIsInvalid_thenReturnsValidationError() throws Exception { + UserDTO dto = new UserDTO(); + dto.setName("Valid Name"); + dto.setEmail("not-an-email"); + + mockMvc + .perform( + post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + void whenEmailIsNull_thenReturnsValidationError() throws Exception { + UserDTO dto = new UserDTO(); + dto.setName("Valid Name"); + dto.setEmail(null); + + mockMvc + .perform( + post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } +}