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