Spring Boot Bean Validation example

This commit is contained in:
Tom Hombergs
2018-10-14 14:48:57 +02:00
parent 03f1680103
commit 24d00fae37
39 changed files with 2064 additions and 24 deletions

View File

@@ -0,0 +1,41 @@
package io.reflectoring.validation;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Pattern;
@Entity
public class Input {
@Id
@GeneratedValue
private Long id;
@Min(1)
@Max(10)
private int numberBetweenOneAndTen;
// Note that this is actually not a valid IP address pattern, since
// it allows values greater than 255 per octet.
@Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")
private String ipAddress;
public int getNumberBetweenOneAndTen() {
return numberBetweenOneAndTen;
}
public void setNumberBetweenOneAndTen(int numberBetweenOneAndTen) {
this.numberBetweenOneAndTen = numberBetweenOneAndTen;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
}

View File

@@ -0,0 +1,56 @@
package io.reflectoring.validation;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import io.reflectoring.validation.service.OnCreate;
import io.reflectoring.validation.service.OnUpdate;
@Entity
public class InputWithCustomValidator {
@Id
@GeneratedValue
@NotNull(groups = OnUpdate.class)
@Null(groups = OnCreate.class)
private Long id;
@Min(1)
@Max(10)
@Column
private int numberBetweenOneAndTen;
@IpAddress
@Column
private String ipAddress;
public int getNumberBetweenOneAndTen() {
return numberBetweenOneAndTen;
}
public void setNumberBetweenOneAndTen(int numberBetweenOneAndTen) {
this.numberBetweenOneAndTen = numberBetweenOneAndTen;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,25 @@
package io.reflectoring.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = IpAddressValidator.class)
@Documented
public @interface IpAddress {
String message() default "{IpAddress.invalid}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}

View File

@@ -0,0 +1,32 @@
package io.reflectoring.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class IpAddressValidator implements ConstraintValidator<IpAddress, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern = Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");
Matcher matcher = pattern.matcher(value);
try {
if (!matcher.matches()) {
return false;
} else {
for (int i = 1; i <= 4; i++) {
int octet = Integer.valueOf(matcher.group(i));
if (octet > 255) {
return false;
}
}
return true;
}
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
package io.reflectoring.validation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ValidationApplication {
public static void main(String[] args) {
SpringApplication.run(ValidationApplication.class, args);
}
}

View File

@@ -0,0 +1,39 @@
package io.reflectoring.validation.controller.controlleradvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
class ErrorHandlingControllerAdvice {
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onConstraintValidationException(ConstraintViolationException e) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (ConstraintViolation violation : e.getConstraintViolations()) {
error.getViolations().add(new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
}
return error;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
error.getViolations().add(new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
}
return error;
}
}

View File

@@ -0,0 +1,17 @@
package io.reflectoring.validation.controller.controlleradvice;
import java.util.ArrayList;
import java.util.List;
public class ValidationErrorResponse {
private List<Violation> violations = new ArrayList<>();
public List<Violation> getViolations() {
return violations;
}
public void setViolations(List<Violation> violations) {
this.violations = violations;
}
}

View File

@@ -0,0 +1,21 @@
package io.reflectoring.validation.controller.controlleradvice;
public class Violation {
private final String fieldName;
private final String message;
public Violation(String fieldName, String message) {
this.fieldName = fieldName;
this.message = message;
}
public String getFieldName() {
return fieldName;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.validation.repository;
import io.reflectoring.validation.Input;
import org.springframework.data.repository.CrudRepository;
public interface ValidatingRepository extends CrudRepository<Input, Long> {
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.validation.repository;
import io.reflectoring.validation.InputWithCustomValidator;
import org.springframework.data.repository.CrudRepository;
public interface ValidatingRepositoryWithCustomValidator extends CrudRepository<InputWithCustomValidator, Long> {
}

View File

@@ -0,0 +1,4 @@
package io.reflectoring.validation.service;
public interface OnCreate {
}

View File

@@ -0,0 +1,4 @@
package io.reflectoring.validation.service;
public interface OnUpdate {
}

View File

@@ -0,0 +1,38 @@
package io.reflectoring.validation.service;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
import io.reflectoring.validation.Input;
import org.springframework.stereotype.Service;
@Service
public class ProgrammaticallyValidatingService {
private Validator validator;
public ProgrammaticallyValidatingService(Validator validator) {
this.validator = validator;
}
public void validateInput(Input input) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Input>> violations = validator.validate(input);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
public void validateInputWithInjectedValidator(Input input) {
Set<ConstraintViolation<Input>> violations = validator.validate(input);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}

View File

@@ -0,0 +1,17 @@
package io.reflectoring.validation.service;
import javax.validation.Valid;
import io.reflectoring.validation.Input;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated
class ValidatingService {
void validateInput(@Valid Input input){
// do something
}
}

View File

@@ -0,0 +1,23 @@
package io.reflectoring.validation.service;
import javax.validation.Valid;
import io.reflectoring.validation.InputWithCustomValidator;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated
class ValidatingServiceWithGroups {
@Validated(OnCreate.class)
void validateForCreate(@Valid InputWithCustomValidator input){
// do something
}
@Validated(OnUpdate.class)
void validateForUpdate(@Valid InputWithCustomValidator input){
// do something
}
}

View File

@@ -0,0 +1 @@
IpAddress.invalid=Invalid IP Address: "${validatedValue}"!

View File

@@ -0,0 +1,16 @@
package io.reflectoring.validation;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class IpAddressValidatorTest {
@Test
void test(){
IpAddressValidator validator = new IpAddressValidator();
assertTrue(validator.isValid("111.111.111.111", null));
assertFalse(validator.isValid("111.foo.111.111", null));
assertFalse(validator.isValid("111.111.256.111", null));
}
}

View File

@@ -0,0 +1,42 @@
package io.reflectoring.validation;
import javax.validation.ConstraintViolationException;
import io.reflectoring.validation.service.ProgrammaticallyValidatingService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ProgrammaticallyValidatingServiceTest {
@Autowired
private ProgrammaticallyValidatingService service;
@Test
void whenInputIsInvalid_thenThrowsException(){
Input input = new Input();
input.setNumberBetweenOneAndTen(0);
input.setIpAddress("invalid");
assertThrows(ConstraintViolationException.class, () -> {
service.validateInput(input);
});
}
@Test
void givenInjectedValidator_whenInputIsInvalid_thenThrowsException(){
Input input = new Input();
input.setNumberBetweenOneAndTen(0);
input.setIpAddress("invalid");
assertThrows(ConstraintViolationException.class, () -> {
service.validateInputWithInjectedValidator(input);
});
}
}

View File

@@ -0,0 +1,16 @@
package io.reflectoring.validation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationApplicationTests {
@Test
public void contextLoads() {
}
}

View File

@@ -0,0 +1,38 @@
package io.reflectoring.validation.controller.parameters;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidateParametersController.class)
class ValidateParametersControllerTest {
@Autowired
private MockMvc mvc;
@Test
void whenPathVariableIsInvalid_thenReturnsStatus400() throws Exception {
mvc.perform(get("/validatePathVariable/3"))
.andExpect(status().isBadRequest());
}
@Test
void whenRequestParameterIsInvalid_thenReturnsStatus400() throws Exception {
mvc.perform(get("/validateRequestParameter")
.param("param", "3"))
.andExpect(status().isBadRequest());
}
@Test
void whenPathVariableIsValid_thenReturnsStatus200() throws Exception {
mvc.perform(get("/validatePathVariable/10"))
.andExpect(status().isOk());
}
}

View File

@@ -0,0 +1,76 @@
package io.reflectoring.validation.controller.requestbody;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.reflectoring.validation.Input;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidateRequestBodyController.class)
class ValidateRequestBodyControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void whenInputIsInvalid_thenReturnsStatus400() throws Exception {
Input input = invalidInput();
String body = objectMapper.writeValueAsString(input);
mvc.perform(post("/validateBody")
.contentType("application/json")
.content(body))
.andExpect(status().isBadRequest());
}
private Input invalidInput() {
Input input = new Input();
input.setIpAddress("invalid");
input.setNumberBetweenOneAndTen(99);
return input;
}
@Test
void whenInputIsInvalid_thenReturnsStatus400WithErrorObject() throws Exception {
Input input = invalidInput();
String body = objectMapper.writeValueAsString(input);
MvcResult result = mvc.perform(post("/validateBody")
.contentType("application/json")
.content(body))
.andExpect(status().isBadRequest())
.andReturn();
assertThat(result.getResponse().getContentAsString()).contains("violations");
}
@Test
void whenInputIsValid_thenReturnsStatus200() throws Exception {
Input input = validInput();
String body = objectMapper.writeValueAsString(input);
mvc.perform(post("/validateBody")
.contentType("application/json")
.content(body))
.andExpect(status().isOk());
}
private Input validInput() {
Input input = new Input();
input.setIpAddress("255.255.255.255");
input.setNumberBetweenOneAndTen(10);
return input;
}
}

View File

@@ -0,0 +1,42 @@
package io.reflectoring.validation.repository;
import javax.persistence.EntityManager;
import javax.validation.ConstraintViolationException;
import io.reflectoring.validation.Input;
import io.reflectoring.validation.repository.ValidatingRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(SpringExtension.class)
@DataJpaTest
class ValidatingRepositoryTest {
@Autowired
private ValidatingRepository repository;
@Autowired
private EntityManager entityManager;
@Test
void whenInputIsInvalid_thenThrowsException() {
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
repository.save(input);
entityManager.flush();
});
}
private Input invalidInput() {
Input input = new Input();
input.setNumberBetweenOneAndTen(0);
input.setIpAddress("invalid");
return input;
}
}

View File

@@ -0,0 +1,44 @@
package io.reflectoring.validation.repository;
import javax.persistence.EntityManager;
import javax.validation.ConstraintViolationException;
import io.reflectoring.validation.InputWithCustomValidator;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Java6Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@DataJpaTest
class ValidatingRepositoryWithCustomValidatorTest {
@Autowired
private ValidatingRepositoryWithCustomValidator repository;
@Autowired
private EntityManager entityManager;
@Test
void whenInputIsInvalid_thenThrowsException() {
InputWithCustomValidator input = invalidInput();
try {
repository.save(input);
entityManager.flush();
Assertions.fail("expected ConstraintViolationException");
} catch (ConstraintViolationException e) {
assertThat(e.getConstraintViolations()).hasSize(2);
}
}
private InputWithCustomValidator invalidInput() {
InputWithCustomValidator input = new InputWithCustomValidator();
input.setNumberBetweenOneAndTen(0);
input.setIpAddress("invalid");
return input;
}
}

View File

@@ -0,0 +1,43 @@
package io.reflectoring.validation.service;
import javax.validation.ConstraintViolationException;
import io.reflectoring.validation.Input;
import io.reflectoring.validation.service.ValidatingService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceTest {
@Autowired
private ValidatingService service;
@Test
void whenInputIsValid_thenThrowsNoException(){
Input input = new Input();
input.setNumberBetweenOneAndTen(5);
input.setIpAddress("111.111.111.111");
service.validateInput(input);
// then no exception
}
@Test
void whenInputIsInvalid_thenThrowsException(){
Input input = new Input();
input.setNumberBetweenOneAndTen(0);
input.setIpAddress("invalid");
assertThrows(ConstraintViolationException.class, () -> {
service.validateInput(input);
});
}
}

View File

@@ -0,0 +1,45 @@
package io.reflectoring.validation.service;
import javax.validation.ConstraintViolationException;
import io.reflectoring.validation.InputWithCustomValidator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceWithGroupsTest {
@Autowired
private ValidatingServiceWithGroups service;
@Test
void whenInputIsInvalidForCreate_thenThrowsException() {
InputWithCustomValidator input = validInput();
input.setId(42L);
assertThrows(ConstraintViolationException.class, () -> {
service.validateForCreate(input);
});
}
private InputWithCustomValidator validInput() {
InputWithCustomValidator input = new InputWithCustomValidator();
input.setNumberBetweenOneAndTen(1);
input.setIpAddress("111.111.111.111");
return input;
}
@Test
void whenInputIsInvalidForUpdate_thenThrowsException() {
InputWithCustomValidator input = validInput();
input.setId(null);
assertThrows(ConstraintViolationException.class, () -> {
service.validateForUpdate(input);
});
}
}