Previous Post : Part Seven: Moving To Production
Get the Code: https://github.com/iainporter/rest-java
The introduction of JSR 303 for validation really simplifies the process of validation. It consists of a meta data model using annotations and an API for running validations against annotated classes.
It cleanly separates the concerns and prevents pojo classes from being cluttered with custom validation code.
In this rest sample project the DTO objects that comprise the API layer are annotated with validations. The service tier is then responsible for handling any validation failures and wrapping them in ValidationExceptions.
Using this approach makes it possible to publish your API with all of the DTO classes and service interfaces and rely on the consumer of the API to make their own decisions on Validation handling.
JSR 303 Implementation
The dependencies in gradle:
'org.hibernate:hibernate-validator:4.3.1.Final'
'javax.validation:validation-api:1.1.0.Final'
for maven:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.0.1.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
Applying Constraints
@XmlRootElement public class CreateUserRequest { @NotNull @Valid private ExternalUser user; @NotNull @Valid private PasswordRequest password; public CreateUserRequest() { } public CreateUserRequest(final ExternalUser user, final PasswordRequest password) { this.user = user; this.password = password; } public ExternalUser getUser() { return user; } public void setUser(ExternalUser user) { this.user = user; } public PasswordRequest getPassword() { return password; } public void setPassword(PasswordRequest password) { this.password = password; } }
The two properties user and password can not be null. The @Valid constraint performs validation recursively on the objects.
The relevant part of ExternalUser.java
@XmlRootElement public class ExternalUser implements Principal { private String id; @Length(max=50) private String firstName; @Length(max=50) private String lastName; @NotNull @Email private String emailAddress; ............
and PasswordRequest.java
@XmlRootElement public class PasswordRequest { @Length(min=8, max=30) @NotNull private String password; public PasswordRequest() {} public PasswordRequest(final String password) { this.password = password; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
@Length is part of the Bean Validation spec whilst @Email is a Hibernate custom validation
Validating Constraints
First we must create an instance of Validator and then pass it into any service that requires it.
@Autowired public UserServiceImpl(UsersConnectionRepository usersConnectionRepository, Validator validator) { this(validator); this.jpaUsersConnectionRepository = usersConnectionRepository; ((JpaUsersConnectionRepository)this.jpaUsersConnectionRepository).setUserService(this); }
We can call validate on any request object in the service class and throw a Validation Exception if there are any failures
@Transactional public AuthenticatedUserToken createUser(CreateUserRequest request, Role role) { validate(request); User searchedForUser = userRepository.findByEmailAddress(request.getUser().getEmailAddress()); if (searchedForUser != null) { throw new DuplicateUserException(); } User newUser = createNewUser(request, role); AuthenticatedUserToken token = new AuthenticatedUserToken(newUser.getUuid().toString(), createAuthorizationToken(newUser).getToken()); userRepository.save(newUser); return token; }
The validate method
protected void validate(Object request) { Set<? extends ConstraintViolation<?>> constraintViolations = validator.validate(request); if (constraintViolations.size() > 0) { throw new ValidationException(constraintViolations); } }
The Validation Exception class wraps any errors in a response object to return to the client to provide a meaningful contextual error response.
public class ValidationException extends WebApplicationException { private final int status = 400; private String errorMessage; private String developerMessage; private List<ValidationError> errors = new ArrayList<ValidationError>(); public ValidationException() { errorMessage = "Validation Error"; developerMessage = "The data passed in the request was invalid. Please check and resubmit"; } public ValidationException(String message) { super(); errorMessage = message; } public ValidationException(Set<? extends ConstraintViolation<?>> violations) { this(); for(ConstraintViolation<?> constraintViolation : violations) { ValidationError error = new ValidationError(); error.setMessage(constraintViolation.getMessage()); error.setPropertyName(constraintViolation.getPropertyPath().toString()); error.setPropertyValue(constraintViolation.getInvalidValue() != null ? constraintViolation.getInvalidValue().toString() : null); errors.add(error); } } @Override public Response getResponse() { return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE).entity(getErrorResponse()).build(); } public ErrorResponse getErrorResponse() { ErrorResponse response = new ErrorResponse(); response.setApplicationMessage(developerMessage); response.setConsumerMessage(errorMessage); response.setValidationErrors(errors); return response; } }
Testing Validation
Testing is easy. Just create an instance of Validator and pass it the object to test.public class PasswordRequestTest { protected Validator validator; @Before public void setUp() { validator = Validation.buildDefaultValidatorFactory().getValidator(); } @Test public void validPassword() { PasswordRequest request = new PasswordRequest("password"); Set<ConstraintViolation<PasswordRequest>> constraints = validator.validate(request); assertThat(constraints.size(), is(0)); } public void passwordTooShort() { PasswordRequest request = new PasswordRequest(RandomStringUtils.randomAlphanumeric(7)); Set<ConstraintViolation<PasswordRequest>> constraints = validator.validate(request); assertThat(constraints.size(), is(1)); } public void passwordTooLong() { PasswordRequest request = new PasswordRequest(RandomStringUtils.randomAlphanumeric(36)); Set<ConstraintViolation<PasswordRequest>> constraints = validator.validate(request); assertThat(constraints.size(), is(1)); } }
Testing the API
To test is against the running application start up the server by executing gradle tomcatRunExecute an curl statement with an invalid request such as the following
curl -v -H "Content-Type: application/json" -X POST -d '{"user":{"firstName","lastName":"Bar","emailAddress":"@##@.com"}, "password":"123"}' http://localhost:8080/java-rest/user
You should see s result similar to this
HTTP/1.1 400 Bad Request
Server: Apache-Coyote/1.1
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 13 May 2013 21:22:23 GMT
Connection: close
* Closing connection #0
{"errorCode":null,"consumerMessage":"Validation Error","applicationMessage":"The data passed in the request was invalid. Please check and resubmit","validationErrors":[{"propertyName":"user.emailAddress","propertyValue":"@##@.com","message":"not a well-formed email address"},{"propertyName":"password.password","propertyValue":"123","message":"length must be between 8 and 30"}]}
There are lots more cool things you can do with validation. See the spec for more details.