Validating resources using bean validation support

After introducing the request-payload data binding process, we must talk about validation.

Getting ready

The goal of this recipe is to show how to get Spring MVC to reject request body payloads that are not satisfying a bean validation (JSR-303) or not satisfying the constraints of a defined Spring validator implementation.

After the Maven and Spring configuration, we will see how to bind a validator to an incoming request, how to define the validator to perform custom rules, how to set up a JSR-303 validation, and how to handle the validation results.

How to do it…

  1. We added a Maven dependency to the hibernate validator:
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>4.3.1.Final</version>
    </dependency>
  2. A LocalValidatorFactoryBean has been registered in our dispatcher-servlet.xml (cloudstreetmarket-api):
    <bean id="validator" class="org.sfw.validation.beanvalidation.LocalValidatorFactoryBean"/>
  3. The UsersController and TransactionController have seen their POST and PUT method signature altered with the addition of a @Valid annotation on the @RequestBody arguments:
      @RequestMapping(method=PUT)
      @ResponseStatus(HttpStatus.OK)
      public void update(@Valid @RequestBody User user, 
      BindingResult result){
        ValidatorUtil.raiseFirstError(result);
        user = communityService.updateUser(user);
      }

    Note

    Note here the BindingResult object injected as method argument. Also we will present the ValidatorUtil class in about a minute.

  4. Our two CRUD controllers now have a new @InitBinder annotated method:
      @InitBinder
        protected void initBinder(WebDataBinder binder) {
            binder.setValidator(new UserValidator());
        }
  5. This method binds an instance of a created Validator implementation to the requests. Check out the created UserValidator which is Validator implementation:
    package edu.zipcloud.cloudstreetmarket.core.validators;
    import java.util.Map;
    import javax.validation.groups.Default;
    import org.springframework.validation.Errors;
    import org.springframework.validation.Validator;
    import edu.zc.csm.core.entities.User;
    import edu.zc.csm.core.util.ValidatorUtil;
    public class UserValidator implements Validator {
      @Override
      public boolean supports(Class<?> clazz) {
        return User.class.isAssignableFrom(clazz);
      }
      @Override
      public void validate(Object target, Errors err) {
        Map<String, String> fieldValidation = ValidatorUtil.validate((User)target, Default.class);
        fieldValidation.forEach(
          (k, v) -> err.rejectValue(k, v)
        );
      }
    }
  6. In the User entity, a couple of special annotations have been added:
    @Entity
    @Table(name="users")
    public class User extends ProvidedId<String> implements UserDetails{
      ...
      private String fullName;
      @NotNull
      @Size(min=4, max=30)
      private String email;
      @NotNull
      private String password;
      private boolean enabled = true;
      @NotNull
      @Enumerated(EnumType.STRING)
      private SupportedLanguage language;
      private String profileImg;
    
      @Column(name="not_expired")
      private boolean accountNonExpired;
      @Column(name="not_locked")
      private boolean accountNonLocked;
    
      @NotNull
      @Enumerated(EnumType.STRING)
      private SupportedCurrency currency;
    
      private BigDecimal balance;
      ...
    }
  7. We have created the ValidatorUtil class to make those validations easier and to reduce the amount of boilerplate code:
    package edu.zipcloud.cloudstreetmarket.core.util;
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    import javax.validation.ConstraintViolation;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import javax.validation.ValidatorFactory;
    import javax.validation.groups.Default;
    import org.springframework.validation.BindingResult;
    
    public class ValidatorUtil {
        private static Validator validator;
        static {
          ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
          validator = factory.getValidator();
        }

    The following validate method allows us to call for a JSR validation from whichever location that may require it:

    public static <T> Map<String, String> validate(T object, Class<?>... groups) {
      Class<?>[] args = Arrays.copyOf(groups, groups.length + 1);
      args[groups.length] = Default.class;
      return extractViolations(validator.validate(object, args));
    }
    private static <T> Map<String, String> extractViolations(Set<ConstraintViolation<T>> violations) {
      Map<String, String> errors = new HashMap<>();
      for (ConstraintViolation<T> v: violations) {
        errors.put(v.getPropertyPath().toString(), "["+v.getPropertyPath().toString()+"] " + StringUtils.capitalize(v.getMessage()));
      }
      return errors;
      }

    The following raiseFirstError method is not of a specific standard, it is our way of rendering to the client the server side errors:

      public static void raiseFirstError(BindingResult result) {
        if (result.hasErrors()) {
          throw new IllegalArgumentException(result.getAllErrors().get(0).getCode());
        }
    else if (result.hasGlobalErrors()) {
    throw new IllegalArgumentException(result.getGlobalError().getDefaultMessage());
           }
    }
    }
  8. As per Chapter 4, Building a REST API for a Stateless Architecture, the cloudstreetmarket-api's RestExceptionHandler is still configured to handle IllegalArgumentExceptions, rendering them with ErrorInfo formatted responses:
    @ControllerAdvice
    public class RestExceptionHandler extends ResponseEntityExceptionHandler {
      @Autowired
      private ResourceBundleService bundle;
       @Override
      protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
        HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorInfo errorInfo = null;
        if(body!=null && bundle.containsKey(body.toString())){
            String key = body.toString();
            String localizedMessage = bundle.get(key);
            errorInfo = new ErrorInfo(ex, localizedMessage, key, status);
        }
        else{
          errorInfo = new ErrorInfo(ex, (body!=null)? body.toString() : null, null, status);
        }
    return new ResponseEntity<Object>(errorInfo, headers, status);
    }
      @ExceptionHandler({ InvalidDataAccessApiUsageException.class, DataAccessException.class, IllegalArgumentException.class })
      protected ResponseEntity<Object> handleConflict(final RuntimeException ex, final WebRequest request) {
          return handleExceptionInternal(ex, I18N_API_GENERIC_REQUEST_PARAMS_NOT_VALID, new HttpHeaders(), BAD_REQUEST, request);
        }
    }
  9. Navigating through the UI improvements, you will notice a new form for updating the user's Preferences. This form is accessible via the Login menu, as shown in the following screenshots:
    How to do it…
    How to do it…
  10. In this user Preferences form, when the frontend validations are deactivated (frontend validations will be developed in the last recipe of this chapter), not filling the e-mail field results in the following (customizable) ErrorInfo object in the HTTP response:
    {"error":"[email] Size must be between 4 and 30",
    "message":"The request parameters were not valid!",
    "i18nKey":"error.api.generic.provided.request.parameters.not.valid",
    "status":400,
    "date":"2016-01-05 05:59:26.584"}
  11. On the frontend side, in order to handle this error, the accountController (in account_management.js) is instantiated with a dependency to a custom errorHandler factory. The code is as follows:
    cloudStreetMarketApp.controller('accountController', function ($scope, $translate, $location, errorHandler, accountManagementFactory, httpAuth, genericAPIFactory){
          $scope.form = {
          id: "",
        email: "",
        fullName: "",
        password: "",
        language: "EN",
        currency: "",
        profileImg: "img/anon.png"
          };
      ...
    }
  12. The accountController has an update method that invokes the errorHandler.renderOnForm method:
      $scope.update = function () {
        $scope.formSubmitted = true;
    
        if(!$scope.updateAccount.$valid) {
            return;
        }
          httpAuth.put('/api/users', JSON.stringify($scope.form)).success(
          function(data, status, headers, config) {
            httpAuth.setCredentials($scope.form.id, $scope.form.password);
          $scope.updateSuccess = true;
          }
        ).error(function(data, status, headers, config) {
            $scope.updateFail = true;
            $scope.updateSuccess = false;
            $scope.serverErrorMessage = errorHandler.renderOnForms(data);
          }
        );
      };
  13. The errorHandler is defined as follows in main_menu.js. It has the capability to pull translations messages from i18n codes:
    cloudStreetMarketApp.factory("errorHandler", ['$translate', function ($translate) {
        return {
            render: function (data) {
            if(data.message && data.message.length > 0){
              return data.message;
            }
            else if(!data.message && data.i18nKey && data.i18nKey.length > 0){
              return $translate(data.i18nKey);
              }
            return $translate("error.api.generic.internal");
            },
            renderOnForms: function (data) {
            if(data.error && data.error.length > 0){
              return data.error;
            }
            else if(data.message && data.message.length > 0){
              return data.message;
            }
            else if(!data.message && data.i18nKey && data.i18nKey.length > 0){
              return $translate(data.i18nKey);
            }
            return $translate("error.api.generic.internal");
            }
        }
    }]);

    The Preferences form is as shown here:

    How to do it…

    Tip

    As we said, to simulate this error, frontend validations need to be deactivated. This can be done adding a novalidate attribute to the <form name="updateAccount" … novalidate> markup in user-account.html.

  14. Back in the server side, we have also created a custom validator for the financial Transaction Entity. This validator makes use of the Spring ValidationUtils:
    @Component
    public class TransactionValidator implements Validator {
      @Override
      public boolean supports(Class<?> clazz) {
        return Transaction.class.isAssignableFrom(clazz);
      }
      @Override
      public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "quote", " transaction.quote.empty");
        ValidationUtils.rejectIfEmpty(errors, "user", " transaction.user.empty");
        ValidationUtils.rejectIfEmpty(errors, "type", " transaction.type.empty");
      }
    }

How it works...

Using Spring validator

Spring offers a Validator interface (org.sfw.validation.Validator) for creating components to be injected or instantiated in the layer we want. Therefore, Spring validation components can be used in Spring MVC Controllers. The Validator interface is the following:

public interface Validator {
  boolean supports(Class<?> clazz);
  void validate(Object target, Errors errors);
}

The supports(Class<?> clazz) method is used to assess the domain of a Validator implementation, and also to restrict its use to a specific Type or super-Type.

The validate(Object target, Errors errors) method imposes its standard so that the validation logic of the validator lives in this place. The passed target object is assessed, and the result of the validation is stored in an instance of the org.springframework.validation.Errors interface. A partial preview of the Errors interface is shown here:

public interface Errors {
  ...
  void reject(String errorCode);
  void reject(String errorCode, String defaultMessage);
void reject(String errorCode, Object[] errorArgs, String defaultMessage);
void rejectValue(String field, String errorCode); void rejectValue(String field, String errorCode, String defaultMessage);
void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage);
  void addAllErrors(Errors errors);
  boolean hasErrors();
  int getErrorCount();
  List<ObjectError> getAllErrors();
  ...
}

Using Spring MVC, we have the possibility to bind and trigger a Validator to a specific method-handler. The framework looks for a validator instance bound to the incoming request. We have configured such a binding in our recipe at the fourth step:

  @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.setValidator(new UserValidator());
    }

Tip

We have already used the @InitBinder annotation to attach other objects (formatters) to incoming requests (see the Binding requests, marshalling responses recipe of the Chapter 4, Building a REST API for a Stateless Architecture).

The Binders (org.springframework.validation.DataBinder) allow setting property values onto a target object. Binders also provide support for validation and binding-results analysis.

The DataBinder.validate() method is called after each binding step and this method calls the validate of the primary validator attached to the DataBinder.

The binding-process populates a result object, which is an instance of the org.springframework.validation.BindingResult interface. This result object can be retrieved using the DataBinder.getBindingResult() method.

Actually, a BindingResult implementation is also an Errors implementation (as shown here). We have presented the Errors interface earlier. Check out the following code:

public interface BindingResult extends Errors {
  Object getTarget();
  Map<String, Object> getModel();
  Object getRawFieldValue(String field);
  PropertyEditor findEditor(String field, Class<?> valueType);
  PropertyEditorRegistry getPropertyEditorRegistry();
  void addError(ObjectError error);
  String[] resolveMessageCodes(String errorCode);
  String[] resolveMessageCodes(String errorCode, String field);
  void recordSuppressedField(String field);
  String[] getSuppressedFields();
}

The whole design can be summarized as follows:

We create a validator implementation. When an incoming request comes in for a specific Controller method handler, the request payload is converted into the class that is targeted by the @RequestBody annotation (an Entity in our case). An instance of our validator implementation is bound to the injected @RequestBody object. If the injected @RequestBody object is defined with a @Valid annotation, the framework asks DataBinder to validate the object on each binding step and to store errors in the BindingResultobject of DataBinder.

Finally, this BindingResult object is injected as argument of the method handler, so we can decide what to do with its errors (if any). During the binding process, missing fields and property access exceptions are converted into FieldErrors. These FieldErrors are also stored into the Errors instance. The following error codes are used for FieldErrors:

Missing field error: "required"
Type mismatch error: "typeMismatch"
Method invocation error: "methodInvocation"

When it is necessary to return nicer error messages for the user, a MessageSource helps us to process a lookup and retrieve the right localized message from a MessageSourceResolvable implementation with the following method:

MessageSource.getMessage(org.sfw.context.MessageSourceResolvable, java.util.Locale). 

Tip

The FieldError extends ObjectError and ObjectError extends DefaultMessageSourceResolvable, which is a MessageSourceResolvable implementation.

ValidationUtils

The ValodationUtils utility class (org.sfw.validation.ValidationUtils) provides a couple of convenient static methods for invoking validators and rejecting empty fields. These utility methods allow one-line assertions that also handle at the same time, the population of the Errors objects. In this recipe, the 14th step details our TransactionValidator that makes use of ValidationUtils.

I18n validation errors

The next recipe will focus on internationalization of errors and content. However, let's see how we catch our errors from the controllers and how we display them. The update method of UserController has this custom method call on its first line:

  ValidatorUtil.raiseFirstError(result);

We created the ValidatorUtil support class for our needs; the idea was to throw an IllegalArgumentException for any type of error that can be detected by our validator. The ValidatorUtil.raiseFirstError(result) method call can also be found in the TransactionController.update(…) method-handler. This method-handler relies on the TransactionValidator presented in the 14th step.

If you remember this TransactionValidator, it creates an error with a transaction.quote.empty message code when a quote object is not present in the financial Transaction object. An IllegalArgumentException is then thrown with the transaction.quote.empty message detail.

In the next recipe, we will revisit how a proper internationalized JSON response is built and sent back to the client from an IllegalArgumentException.

Using JSR-303/JSR-349 Bean Validation

Spring Framework version 4 and above supports bean validation 1.0 (JSR-303) and bean validation 1.1 (JSR-349). It also adapts this bean validation to work with the Validator interface, and it allows the creation of class-level validators using annotations.

The two specifications, JSR-303 and JSR-349, define a set of constraints applicable to beans, as annotations from the javax.validation.constraints package.

Generally, a big advantage of using the code from specifications instead of the code from implementations is that we don't have to know which implementation is used. Also, the implementation can always potentially be replaced with another one.

Bean validation was originally designed for persistent beans. Even if the specification has a relatively low coupling to JPA, the reference implementation stays Hibernate validator. Having a persistence provider that supports those validation specifications is definitely an advantage. Now with JPA2, the persistent provider automatically calls for JSR-303 validation before persisting. Ensuring such validations from two different layers (controller and model) raises our confidence level.

On-field constraint annotations

We defined the @NotNull and @Size JSR-303 annotations on the presented User entity. There are obviously more than two annotations to be found in the specification.

Here's a table summarizing the package of annotations (javax.validation.constraints) in JEE7:

Annotation Type

Description

AssertFalse

The annotated element must be false.

AssertFalse.List

Defines several AssertFalse annotations on the same element.

AssertTrue

The annotated element must be true.

AssertTrue.List

Defines several AssertTrue annotations on the same element.

DecimalMax

The annotated element must be a number whose value must be lower or equal to the specified maximum.

DecimalMax.List

Defines several DecimalMax annotations on the same element.

DecimalMin

The annotated element must be a number whose value must be higher or equal to the specified minimum.

DecimalMin.List

Defines several DecimalMin annotations on the same element.

Digits

The annotated element must be a number within the accepted range. Supported types are: BigDecimal, BigInteger, CharSequence, byte, short, int, long, and their respective wrapper types. However, null elements are considered valid.

Digits.List

Defines several Digits annotations on the same element.

Future

The annotated element must be a date in the future.

Future.List

Defines several Future annotations on the same element.

Max

The annotated element must be a number whose value must be lower than or equal to the specified maximum.

Max.List

Defines several Max annotations on the same element.

Min

The annotated element must be a number whose value must be higher than or equal to the specified minimum.

Min.List

Defines several Min annotations on the same element.

NotNull

The annotated element must not be null.

NotNull.List

Defines several NotNull annotations on the same element.

Past

The annotated element must be a date in the past.

Past.List

Defines several Past annotations on the same element.

Pattern

The annotated CharSequence must match the specified regular expression.

Pattern.List

Defines several Pattern annotations on the same element.

Size

The annotated element size must be between the specified boundaries (included).

Size.List

Defines several Size annotations on the same element.

Implementation-specific constraints

Bean validation implementations can also go beyond the specification and offer their set of extra validation annotations. Hibernate validator has a few interesting ones such as @NotBlank, @SafeHtml, @ScriptAssert, @CreditCardNumber, @Email, and so on. These are all listed from the hibernate documentation accessible at the following URL

http://docs.jboss.org/hibernate/validator/4.3/reference/en-US/html_single/#table-custom-constraints

LocalValidator (reusable)

We have defined the following validator bean in our Spring context:

<bean id="validator" class="org.sfw.validation.beanvalidation.LocalValidatorFactoryBean"/>

This bean produces validator instances that implement JSR-303 and JSR-349. You can configure a specific provider class here. By default, Spring looks in the classpath for the Hibernate Validator JAR. Once this bean is defined, it can be injected wherever it is needed.

We have injected such validator instances in our UserValidator and this makes it compliant with JSR-303 and JSR-349.

For internationalization, the validator produces its set of default message codes. These default message codes and values look like the following ones:

javax.validation.constraints.Max.message=must be less than or equal to {value}
javax.validation.constraints.Min.message=must be greater than or equal to {value}
javax.validation.constraints.Pattern.message=must match "{regexp}"
javax.validation.constraints.Size.message=size must be between {min} and {max}

Feel free to override them in your own resource files!

There's more…

In this section we highlight a few validation concepts and components that we didn’t explain.

ValidationUtils

The ValidationUtils Spring utility class provides convenient static methods for invoking a Validator and rejecting empty fields populating the error object in one line:

http://docs.spring.io/spring/docs/3.1.x/javadoc-api/org/springframework/validation/ValidationUtils.html

Creating a custom validator

It can sometimes be useful to create a specific validator that has its own annotation. Check link, it should get us to:

http://howtodoinjava.com/2015/02/12/spring-mvc-custom-validator-example/

The Spring reference on validation

The best source of information remains the Spring reference on Validation. Check link, it should get us to:

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset