Image: Stuart Miles |
In the last post, this series identified the required steps to support persistence using Spring Data JPA in API powered by Spring MVC. It taught us that users can make a request using JSON representation of some java bean in our application. Then they expect that their requests trigger some actions (e.g. singing up a user) and that the API will return adequate responses.
However, how can the API make sure that the request is valid in the first place?
Example of Invalid Request
A client of the signup webservice we wrote in the last post might make this request
curl -H "Content-Type: application/json; charset=utf-8" --data '{"email":"post", "password":"ww"}' http://localhost:8080/services/1/user
- email format is not correct.
- username is null
- password is too short
Hence, we need a code somewhere in our application to add constrains on data been provided by users.
JSR 303: BEAN VALIDATION
Dependancy
org.hibernate hibernate-validator 5.1.3.Final
Validator Bean
In Service/com.coffeebeans.config.ServicesConfig add the following bean
@Bean public Validator validator() { return new LocalValidatorFactoryBean(); }
Annotation entity classes
Now, change com.coffeebeans.domain.request.user.UserSignupRequest to be as followingpackage com.coffeebeans.domain.request.user; import lombok.Data; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; /** * Created by muhamadto on 12/07/2015. */ @Data public class UserSignupRequest { @NotNull @Email private String email; @NotNull private String username; @NotNull @Length(min=8, max=16) private String password; }This class showed us 3 validation annotation
- javax.validation.constraints.NotNull, which means that the client must provide a value for this field.
- org.hibernate.validator.constraints.Length, using this annotation we can specify min and max length of a field (e.g. password).
- org.hibernate.validator.constraints.Email, makes sure that the supplied value for the field annotate with it is a valid email format.
NOTE: It would make sense to add length check on the username and email among other checks. However, covering the annotations in JSR303 is not the goal of this post. The scope of this lesson is rather, to explain how data validation can be enabled in Spring RESTful API.
Create validation Exception
This exception is going to be returned to the services' clients if the requests were invalid.package com.coffeebeans.exception.validation; import com.coffeebeans.api.error.ValidationError; import javax.validation.ConstraintViolation; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Created by muhamadto on 12/07/2015. */ public class ValidationException extends RuntimeException{ private List<validationerror> validationErrors = new ArrayList<validationerror>(); public ValidationException (){ super(); } public ValidationException (String message){ super(message); } public ValidationException(String message, Throwable cause) { super(message, cause); } public ValidationException(Throwable cause) { super(cause); } protected ValidationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } public ValidationException(Set> constraintViolations) { Iterator> iterator = constraintViolations.iterator(); while(iterator.hasNext()){ ConstraintViolation invalidConstraint = iterator.next(); ValidationError validationError = new ValidationError(); validationError.setMessage(invalidConstraint.getMessage()); validationError.setPropertyName(String.valueOf(invalidConstraint.getPropertyPath())); validationError.setPropertyValue(String.valueOf(invalidConstraint.getInvalidValue())); validationErrors.add(validationError); } } public List<validationerror> getValidationErrors() { return validationErrors; } }This Exception class uses another class that comprises of:
- Name of the field with invalid value;
- The value that violated the constrains
- custom message
package com.coffeebeans.api.error; import lombok.Data; /** * Created by muhamadto on 12/07/2015. */ public @Data class ValidationError { private String propertyName; private String propertyValue; private String message; }
Use Validator Bean
com.coffeebeans.service.base.BaseService is a good place to write a generic method to validate if data is violating the constrains dictate through the metadata.@Autowired Validator validator; protected void validate(Object request) throws ValidationException{ Set<? extends ConstraintViolation<?>> constraintViolations = validator.validate(request); if (!CollectionUtils.isEmpty(constraintViolations)) { throw new ValidationException(constraintViolations); } }The app autowired the validator bean and used it to check the data.
Perform the validation on requests
As an example, let's validate the UserSignupRequest from last post. Make a call to com.coffeebeans.service.base.BaseService#validate() in the first line of com.coffeebeans.service.user.UserServiceImpl#createUser(). The logic should be like this.@Override @Transactional public UserResponse createUser(UserSignupRequest userSignupRequest, Role role) { validate(userSignupRequest); UserResponse newUser; final String email = userSignupRequest.getEmail().toLowerCase(); if(this.userRepository.findByEmail(email) == null){ LOGGER.info("User does not exist - creating a new user [{}].", userSignupRequest.getUsername()); User user = new User(userSignupRequest); user.setRole(role); Date now = new Date(); user.setInsertDate(now); user.setLastModified(now); user = this.userRepository.save(user); newUser = UserResponse.convert(user); LOGGER.debug("Created new user [{}].", user); } else{ LOGGER.error("Duplicate user located [{}], exception raised with appropriate HTTP response code.", email); throw new DuplicateEntryException("User already exists."); } return newUser; }
Exception Handling
In com.coffeebeans.controller.base.BaseController add the following snippet.@ResponseBody @ExceptionHandler(ValidationException.class) @ResponseStatus(value = HttpStatus.BAD_REQUEST) ErrorResponse handleException(ValidationException e){ return new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), e.getMessage(), "Bad Request", e.getValidationErrors()); }The com.coffeebeans.api.error.ErrorResponse now has a new constructor which takes additional list of the validation errors.
package com.coffeebeans.api.error; import lombok.Data; import java.util.ArrayList; import java.util.List; /** * Created by muhamadto on 12/07/2015. */ @Data public class ErrorResponse { private String errorCode; private String consumerMessage; private String applicationMessage; private List<validationerror> validationErrors = new ArrayList<>(); public ErrorResponse(){} public ErrorResponse(String errorCode, String consumerMessage, String applicationMessage){ this(errorCode, consumerMessage, applicationMessage, null); } public ErrorResponse(String errorCode, String consumerMessage, String applicationMessage, List<validationerror> validationErrors){ this.errorCode = errorCode; this.consumerMessage = consumerMessage; this.applicationMessage = applicationMessage; this.validationErrors = validationErrors; } }
Using the invalid request again
Try to send this request at the begging of this post again
Response
- Request
curl -H "Content-Type: application/json; charset=utf-8" --data '{"email":"post", "password":"ww"}' http://localhost:8080/services/1/user
{ "errorCode": "400" "consumerMessage"": null "applicationMessage": "Bad Request" "validationErrors":[ { "propertyName": "email" "propertyValue": "post" "message": "not a well-formed email address" },{ "propertyName": "password" "propertyValue": "ww" "message": "length must be between 8 and 16" },{ "propertyName": "username" "propertyValue: "null" "message": "may not be null" } ] }
This comment has been removed by a blog administrator.
ReplyDeleteYour Affiliate Money Printing Machine is waiting -
ReplyDeleteAnd getting it running is as easy as 1...2...3!
Here are the steps to make it work...
STEP 1. Choose which affiliate products the system will advertise
STEP 2. Add some push button traffic (it LITERALLY takes JUST 2 minutes)
STEP 3. See how the system explode your list and sell your affiliate products on it's own!
Do you want to start making profits?
You can test-drive the system for yourself risk free...