Applying constraint definitions

As we have seen, applying built-in constraints to value objects is almost painless. What happens if you want a constraint that is not covered by the built-in types? Bean Validation allows the developer to write custom constraints.

Let's review a value object that has an entity relationship with another one. Here is the code for the Country object:

package je7hb.beanvalidation.essentials;
import org.hibernate.validator.constraints.NotEmpty;

public class Country {
    private String isoName;
    @NotEmpty
    public String getISOName() { return isoName; }
    public Country() { }
}

This is the Address object, the master of the detail:

package je7hb.beanvalidation.essentials;
import javax.validation.Valid;
import javax.validation.constraints.*;

public class Address {
    private String flatNo;
    private String street1;
    private String street2;
    private String city;
    private String postalCode;
    private Country country;

    @NotNull @Size(max=50)
    public String getStreet1() { return street1; }
    @NotNull @Size(max=50)
    public String getStreet2() { return street2; }

    @PostalCode(
    message="Wrong postal code")
    public String getPostalCode() { return postalCode; }

    @NotNull @Valid
    public Country getString() { return country; }

  // Constructor & setter methods ommitted 
}

The value object for class Address has field members: flatNo , street1 , street2 , city , postalCode and country in order to represent a person's living or home address. We have applied constraint annotations applied to the getter methods. The @Size and @NotNull constraints are applied to street1 and street2 respectively, to ensure that the backing store field sizes are checked.

The getCountry() method is an example of declaring a delegating constraint on a dependent object. The annotation @javax.annotation.Valid cascades the validation checking to the dependent object property on the instance. We can also supply the @Valid constraint to method calls to a method parameter or the instance returned from a method call. We shall learn how to apply method validation later in this chapter.

The @PostalCode validation and annotation is an example of a custom constraint.

Custom validators

Writing a custom validator is fairly straightforward. The first step is to write an annotation that depends on the @javax.validation.Constraint.

Here is the @PostCode constraint annotation:

package je7hb.beanvalidation.essentials;
import javax.validation.*;
import java.lang.annotation.*;
import static java.lang.annotation.RetentionPolicy.*;
import static java.lang.annotation.ElementType.*;

@Documented
@Constraint(validatedBy = PostCodeValidator.class)
@Target({ 
  METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, 
  PARAMETER })
@Retention(RUNTIME)
public @interface PostalCode {
    String message() default 
  "{je7hb.beanvalidation.essentials.PostalCode.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String country() default "gb";
}

The @Constraint refers to the class that provides the custom validator, in this case PostCodeValidator. The annotation is declared as runtime-type retention with injection points for constructors, fields, methods, parameters, and other constraint annotations. The message declaration refers to a key inside a resource bundle to look up an internationalized text.

Your custom annotation can define extra parameters and in the @PostalCode we define a country parameter to define the locale for validating the postal code.

The custom constraint PostCodeValidator extends a parameterized type interface javax.validation.ConstraintValidator:

package je7hb.beanvalidation.essentials;
import javax.validation.*;
import java.util.regex.*;

public class PostCodeValidator 
implements ConstraintValidator<PostalCode,String> {
private String country;
    private Pattern pattern =
        Pattern.compile(
"[A-Z][A-Z]\d{1,2}[ 	]*\d{1,2}[A-Z][A-Z]");

    @Override
    public void initialize(PostalCode postalCode) {
this.country = postalCode.country();}

    @Override
    public boolean isValid(String value,
       ConstraintValidatorContext context) {
        if ( value == null) {
            return true;
        }
        Matcher m = pattern.matcher(value.toUpperCase());
        return m.matches();
    }
}

The parameterized interface ConstraintValidator<U extends Annotation,V> defines the logic for a generic type that validates an input generic type V. The generic type must be a Java annotation type A.

There are two methods to implement: initialize() and isValid(). The provider calls the initialize() method with the instance of the annotation. We can access extra annotation parameter values and we save the value as a field in this instance for future use in the next method. Actually, we are not using the extra parameter in the validation in this demonstration.

The provider calls isValid() method with a String value and the javax.validation.ConstraintValidatorContext instance. We always validate null reference pointer values as successful, because we do not want to interfere with @NotNull being applied by the user. The PostCodeValidator class uses a regular pattern to match British postal codes. So we validate the input value against the pattern using a matcher. If the regex matches true, then it must be good.

Tip

Custom validators and null

Most custom validations ignore the case, when the element value is a. If the value is null then the validation returns true. If the element value is not null then an attempt is made to validate the element. This is helpful for situations in web application where the user may not yet define the field.

Custom validators

Groups of constraints

We have seen that constraints are grouped together and validated on a value object. The constraints validate on the property or field member of an object instance. Whilst this is good enough for basic properties, we often need flexibility in our applications. Bean Validation, however, provides additional validation for class instances and partial validation for groups of constraints.

Class-level constraints

A constraint can also be applied to the class itself, and then it is called a class-level constraint. The class-level constraints permit the ability to inspect more than one single property of the class. Therefore, they provide a means to validate associated fields or properties. In order to apply a class-level constraint annotation, you declare it on the class itself.

We shall write a new class-level constraint to validate only UK post codes for an address value object.

First we need a different value type, AddressGroup:

@ISOPostalCode(message="ISO uk or gb only")
public class AddressGroup {
    private String flatNo;
    private String street1;
    private String street2;
    private String city;
    private String postalCode;
    private Country country;

    @NotNull @Size(max=50)
    public String getStreet1() { return street1; }

    @NotNull @Size(max=50)
    public String getStreet2() { return street2; }

    public String getPostalCode() { return postalCode; }

    @NotNull @Valid
    public Country getCountry() { return country; }

  /* ... */
}

This value object makes use of the custom constraint annotation @ISOPostalCode. Notice that the property postCode no longer has the single property constraintany more.

Let's define our annotation @ISOPostalCode now:

@Documented
@Constraint(validatedBy = ISOPostCodeValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface ISOPostalCode {
    String message() default 
"{je7hb.beanvalidation.essentials.ISOPostalCode.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

We also restrict the use of this annotation to only classes with the @Target(TYPE) definition. This is a design choice, naturally. The annotation refers to the class type that performs the checking: ISOPostCodeValidator.

This is the code for the validator:

package je7hb.beanvalidation.essentials;
import javax.validation.*;
import java.util.regex.*;

public class ISOPostCodeValidator
implements ConstraintValidator<ISOPostalCode,AddressGroup> {
    private Pattern pattern =
        Pattern.compile(
"[A-Z][A-Z]\d{1,2}[ 	]*\d{1,2}[A-Z][A-Z]");

    @Override
    public void initialize(ISOPostalCode annotation) { }

    @Override
    public boolean isValid(AddressGroup value,
               ConstraintValidatorContext context) {
        if (value == null) { return true; }
        String isoName = "";
        if ( value.getCountry() != null ) {
           isoName =
             value.getCountry().getISOName().toUpperCase();
        }
        if ( isoName.equals("UK") || isoName.equals("GB")) {
            Matcher m = pattern.matcher(
              value.getPostalCode());
            return m.matches();
        }
        else return false;
    }
}

The class ISOPostCodeValidator is a type of constraint parameterized with the type ConstraintValidator<ISOPostalCode,AddressGroup>. In short, this constraint validates two dependent properties: the post code and the IOS country name.

The method isValid() already accepts an AddressGroup instance. We verify that the correct international country has been set, and retrieve the ISO name from the dependent Country instance and its country property. If the ISO name is appropriately valid then we can apply the regular expression and attempt to validate the postcode property. If the ISO name is set neither to UK or GB, then the class-level constraint fails validation.

Bean validation can also validate groups of constraints and we will look into this feature next.

Partial validation

Constraints belong to a default group interface javax.validation.groups.Default, when the developer does supply groups annotation parameter. The type Default is actually an empty Java interface. Hence the Bean Validation provider will invoke the constraints attached to all constructors, fields, properties, and methods for a value object. Applications can create partial validation rules by creating separate empty Java interfaces, which denote custom groups.

Suppose we want to validate a car value object from the automotive industry with different validators: one to completely verify the properties and the other to just check some of the details.

We can write a new Car entity like the following:

package je7hb.beanvalidation.cars;
import javax.validation.constraints.*;

public class Car {
    @NotNull(groups=BasicCheck.class)
    private final String carMaker;

    @Min(value=2, groups={BasicCheck.class,
            CompleteCheck.class})
    private int seats;

    @Size(min=4, max=8, groups=BasicCheck.class)
    private String licensePlate;

    @Min(value=500, groups={BasicCheck.class,
            CompleteCheck.class})
    private int engineSize;

    public Car(String carMaker, int seats, 
    String licensePlate) {
        this(carMaker, seats, licensePlate, 0 );
    }

    public Car(final String carMaker, 
    final int seats, 
    final String licensePlate, 
    final int engineSize) {
        this.carMaker = carMaker;
        this.seats = seats;
        this.licensePlate = licensePlate;
        this.engineSize = engineSize;
    }
  
  /* ... */
}

The only addition to value type Car is the group parameter on the constraint annotations, which specifies the interface to associate each constraint with a group. The actual parameter is a variable-length argument and therefore a constraint can be associated with multiple groups.

The Java interfaces representing the groups are extremely simple:

public interface BasicCheck { }
public interface CompleteCheck { }

So a unit to verify these checks to Car instances supplies the references to the group classes for the validation. Here is the unit test code snippet:

package je7hb.beanvalidation.cars;

/* ... omitted imports ... */
public class CarValidatorTest {
    private static Validator validator;

    @BeforeClass
    public static void setUp() { /* ... */ }

    @Test
    public void shouldBasicValidateCar() {
        Car car = new Car("Austin Martin", 0, "AM12457", 0 );
        Set<ConstraintViolation<Car>> constraintViolations
         = validator.validate(car, BasicCheck.class );
        assertEquals(0, constraintViolations.size());
    }

    @Test
    public void shouldCompletelyValidateCar() {
        Car car = new Car("Bentley", 4, "BY4823", 2560 );
        Set<ConstraintViolation<Car>> constraintViolations
      = validator.validate(car, BasicCheck.class,
           CompleteCheck.class);
        assertEquals(0, constraintViolations.size());
    }

    @Test
    public void shouldNotCompletelyValidateCar() {
        Car car = new Car("Sedaca", 0, "XYZ1234", 0 );
        Set<ConstraintViolation<Car>> constraintViolations
        = validator.validate(car, BasicCheck.class,
           CompleteCheck.class );
        assertEquals(2, constraintViolations.size());
    } 
}

The differences between the unit test methods shouldBasicValidateCar() and shouldCompletelyValidateCar() for the test CarValidatorTest are the group interface classes BasicCheck and Complete respectively. We only partially populate properties of the Car instance inside shouldBasicValidateCar(). We also call the validator's validate() method with the group interfaces, which is also a variable-length argument.

The method shouldNotCompletelyValidateCar() verifies the constraints with the group CompleteCheck, which should fail the validation because the Car instance is not correctly constructed. A car cannot have zero seats nor can it have zero engine size.

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

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