Monday, May 13, 2013

Writing REST Services in Java: Part 8 JSR 303 Validation


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 implementation used in the project is Hibernate Validator.

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 tomcatRun

Execute 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.