Hibernate.orgCommunity Documentation
All validation methods on Validator
and
ExecutableValidator
discussed in earlier chapters
also take a var-arg argument groups
. So far we have
been ignoring this parameter, but it is time to have a closer look.
Groups allow you to restrict the set of constraints applied during validation. One use case for validation groups are UI wizards where in each step only a specified subset of constraints should get validated. The groups targeted are passed as var-arg parameters to the appropriate validate method.
Let's have a look at an example. The class
Person
in Example 5.1, “Person” has a
@NotNull
constraint on name.
Since no group is specified for this annotation the default group
javax.validation.groups.Default
is assumed.
When more than one group is requested, the order in which the
groups are evaluated is not deterministic. If no group is specified the
default group javax.validation.groups.Default
is
assumed.
Example 5.1. Person
package org.hibernate.validator.referenceguide.chapter05;
public class Person {
@NotNull
private String name;
public Person(String name) {
this.name = name;
}
// getters and setters ...
}
The class Driver
in Example 5.2, “Driver” extends Person
and adds
the properties age and
hasDrivingLicense. Drivers must be at least 18 years
old (@Min(18)
) and have a driving license
(@AssertTrue
). Both constraints defined on these
properties belong to the group DriverChecks
which
is just a simple tagging interface.
Using interfaces makes the usage of groups type-safe and allows for easy refactoring. It also means that groups can inherit from each other via class inheritance.
Example 5.2. Driver
package org.hibernate.validator.referenceguide.chapter05;
public class Driver extends Person {
@Min(
value = 18,
message = "You have to be 18 to drive a car",
groups = DriverChecks.class
)
public int age;
@AssertTrue(
message = "You first have to pass the driving test",
groups = DriverChecks.class
)
public boolean hasDrivingLicense;
public Driver(String name) {
super( name );
}
public void passedDrivingTest(boolean b) {
hasDrivingLicense = b;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package org.hibernate.validator.referenceguide.chapter05;
public interface DriverChecks {
}
Finally the class Car (Example 5.3, “Car”) has some
constraints which are part of the default group as well as
@AssertTrue
in the group
CarChecks
on the property
passedVehicleInspection which indicates whether a car
passed the road worthy tests.
Example 5.3. Car
package org.hibernate.validator.referenceguide.chapter05;
public class Car {
@NotNull
private String manufacturer;
@NotNull
@Size(min = 2, max = 14)
private String licensePlate;
@Min(2)
private int seatCount;
@AssertTrue(
message = "The car has to pass the vehicle inspection first",
groups = CarChecks.class
)
private boolean passedVehicleInspection;
@Valid
private Driver driver;
public Car(String manufacturer, String licencePlate, int seatCount) {
this.manufacturer = manufacturer;
this.licensePlate = licencePlate;
this.seatCount = seatCount;
}
// getters and setters ...
}
package org.hibernate.validator.referenceguide.chapter05;
public interface CarChecks {
}
Overall three different groups are used in the example:
The constraints on Person.name,
Car.manufacturer,
Car.licensePlate and
Car.seatCount all belong to the
Default
group
The constraints on Driver.age and
Driver.hasDrivingLicense belong to
DriverChecks
The constraint on
Car.passedVehicleInspection belongs to the group
CarChecks
Example 5.4, “Using validation groups” shows how passing different
group combinations to the Validator#validate()
method results in different validation results.
Example 5.4. Using validation groups
// create a car and check that everything is ok with it.
Car car = new Car( "Morris", "DD-AB-123", 2 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );
// but has it passed the vehicle inspection?
constraintViolations = validator.validate( car, CarChecks.class );
assertEquals( 1, constraintViolations.size() );
assertEquals(
"The car has to pass the vehicle inspection first",
constraintViolations.iterator().next().getMessage()
);
// let's go to the vehicle inspection
car.setPassedVehicleInspection( true );
assertEquals( 0, validator.validate( car ).size() );
// now let's add a driver. He is 18, but has not passed the driving test yet
Driver john = new Driver( "John Doe" );
john.setAge( 18 );
car.setDriver( john );
constraintViolations = validator.validate( car, DriverChecks.class );
assertEquals( 1, constraintViolations.size() );
assertEquals(
"You first have to pass the driving test",
constraintViolations.iterator().next().getMessage()
);
// ok, John passes the test
john.passedDrivingTest( true );
assertEquals( 0, validator.validate( car, DriverChecks.class ).size() );
// just checking that everything is in order now
assertEquals(
0, validator.validate(
car,
Default.class,
CarChecks.class,
DriverChecks.class
).size()
);
The first validate()
call in Example 5.4, “Using validation groups” is done using no explicit group. There are
no validation errors, even though the property
passedVehicleInspection is per default
false
. However, the constraint defined on this
property does not belong to the default group.
The next validation using the CarChecks
group
fails until the car passes the vehicle inspection. Adding a driver to the
car and validating against DriverChecks
again
yields one constraint violation due to the fact that the driver has not
yet passed the driving test. Only after setting
passedDrivingTest to true
the
validation against DriverChecks
passes.
The last validate() call finally shows that all constraints are passing by validating against all defined groups.
By default, constraints are evaluated in no particular order, regardless of which groups they belong to. In some situations, however, it is useful to control the order constraints are evaluated.
In the example from Example 5.4, “Using validation groups” it could for instance be required that first all default car constraints are passing before checking the road worthiness of the car. Finally, before driving away, the actual driver constraints should be checked.
In order to implement such a validation order you just need to
define an interface and annotate it with
@GroupSequence
, defining the order in which the
groups have to be validated (see Example 5.5, “Defining a group sequence”). If at least one constraint
fails in a sequenced group none of the constraints of the following groups
in the sequence get validated.
Example 5.5. Defining a group sequence
package org.hibernate.validator.referenceguide.chapter05;
@GroupSequence({ Default.class, CarChecks.class, DriverChecks.class })
public interface OrderedChecks {
}
Groups defining a sequence and groups composing a sequence must
not be involved in a cyclic dependency either directly or indirectly,
either through cascaded sequence definition or group inheritance. If a
group containing such a circularity is evaluated, a
GroupDefinitionException
is raised.
You then can use the new sequence as shown in in Example 5.6, “Using a group sequence”.
Example 5.6. Using a group sequence
Car car = new Car( "Morris", "DD-AB-123", 2 );
car.setPassedVehicleInspection( true );
Driver john = new Driver( "John Doe" );
john.setAge( 18 );
john.passedDrivingTest( true );
car.setDriver( john );
assertEquals( 0, validator.validate( car, OrderedChecks.class ).size() );
Besides defining group sequences, the
@GroupSequence
annotation also allows to redefine
the default group for a given class. To do so, just add the
@GroupSequence
annotation to the class and
specify the sequence of groups which substitute
Default
for this class within the
annotation.
Example 5.7, “Class RentalCar with redefined default
group” introduces a new class
RentalCar
with a redefined default group.
Example 5.7. Class RentalCar
with redefined default
group
package org.hibernate.validator.referenceguide.chapter05;
@GroupSequence({ RentalChecks.class, CarChecks.class, RentalCar.class })
public class RentalCar extends Car {
@AssertFalse(message = "The car is currently rented out", groups = RentalChecks.class)
private boolean rented;
public RentalCar(String manufacturer, String licencePlate, int seatCount) {
super( manufacturer, licencePlate, seatCount );
}
public boolean isRented() {
return rented;
}
public void setRented(boolean rented) {
this.rented = rented;
}
}
package org.hibernate.validator.referenceguide.chapter05;
public interface RentalChecks {
}
With this definition you can evaluate the constraints belonging to
RentalChecks
, CarChecks
and RentalCar
by just requesting the
Default
group as seen in Example 5.8, “Validating an object with redefined default group”.
Example 5.8. Validating an object with redefined default group
RentalCar rentalCar = new RentalCar( "Morris", "DD-AB-123", 2 );
rentalCar.setPassedVehicleInspection( true );
rentalCar.setRented( true );
Set<ConstraintViolation<RentalCar>> constraintViolations = validator.validate( rentalCar );
assertEquals( 1, constraintViolations.size() );
assertEquals(
"Wrong message",
"The car is currently rented out",
constraintViolations.iterator().next().getMessage()
);
rentalCar.setRented( false );
constraintViolations = validator.validate( rentalCar );
assertEquals( 0, constraintViolations.size() );
Since there must no cyclic dependency in the group and group
sequence definitions one cannot just add
Default
to the sequence redefining
Default
for a class. Instead the class itself
has to be added!
The Default
group sequence overriding is
local to the class it is defined on and is not propagated to associated
objects. For the example this means that adding
DriverChecks
to the default group sequence of
RentalCar
would not have any effects. Only the
group Default
will be propagated to the
driver association.
Note that you can control the propagated group(s) by declaring a group conversion rule (see Section 5.4, “Group conversion”).
In addition to statically redefining default group sequences via
@GroupSequence
, Hibernate Validator also provides
an SPI for the dynamic redefinition of default group sequences depending
on the object state.
For that purpose you need to implement the interface
DefaultGroupSequenceProvider
and register this
implementation with the target class via the
@GroupSequenceProvider
annotation. In the rental
car scenario you could for instance dynamically add the
CarChecks
as seen in Example 5.9, “Implementing and using a default group sequence
provider”.
Example 5.9. Implementing and using a default group sequence provider
package org.hibernate.validator.referenceguide.chapter05.groupsequenceprovider;
public class RentalCarGroupSequenceProvider
implements DefaultGroupSequenceProvider<RentalCar> {
@Override
public List<Class<?>> getValidationGroups(RentalCar car) {
List<Class<?>> defaultGroupSequence = new ArrayList<Class<?>>();
defaultGroupSequence.add( RentalCar.class );
if ( car != null && !car.isRented() ) {
defaultGroupSequence.add( CarChecks.class );
}
return defaultGroupSequence;
}
}
package org.hibernate.validator.referenceguide.chapter05.groupsequenceprovider;
@GroupSequenceProvider(RentalCarGroupSequenceProvider.class)
public class RentalCar extends Car {
@AssertFalse(message = "The car is currently rented out", groups = RentalChecks.class)
private boolean rented;
public RentalCar(String manufacturer, String licencePlate, int seatCount) {
super( manufacturer, licencePlate, seatCount );
}
public boolean isRented() {
return rented;
}
public void setRented(boolean rented) {
this.rented = rented;
}
}
What if you wanted to validate the car related checks together with
the driver checks? Of course you could pass the required groups to the
validate call explicitly, but what if you wanted to make these validations
occur as part of the Default
group validation? Here
@ConvertGroup
comes into play which allows you
during cascaded validation to use a different group than the originally
requested one.
Let's have a look at Example 5.10, “@ConvertGroup usage”. Here
@GroupSequence({ CarChecks.class, Car.class })
is used to
combine the car related constraints under the
Default
group (see Section 5.3, “Redefining the default group sequence”). There is also a
@ConvertGroup(from = Default.class, to = DriverChecks.class)
which ensures the Default
group gets converted to
the DriverChecks
group during cascaded validation
of the driver association.
Example 5.10. @ConvertGroup
usage
package org.hibernate.validator.referenceguide.chapter05.groupconversion;
public class Driver {
@NotNull
private String name;
@Min(
value = 18,
message = "You have to be 18 to drive a car",
groups = DriverChecks.class
)
public int age;
@AssertTrue(
message = "You first have to pass the driving test",
groups = DriverChecks.class
)
public boolean hasDrivingLicense;
public Driver(String name) {
this.name = name;
}
public void passedDrivingTest(boolean b) {
hasDrivingLicense = b;
}
// getters and setters ...
}
package org.hibernate.validator.referenceguide.chapter05.groupconversion;
@GroupSequence({ CarChecks.class, Car.class })
public class Car {
@NotNull
private String manufacturer;
@NotNull
@Size(min = 2, max = 14)
private String licensePlate;
@Min(2)
private int seatCount;
@AssertTrue(
message = "The car has to pass the vehicle inspection first",
groups = CarChecks.class
)
private boolean passedVehicleInspection;
@Valid
@ConvertGroup(from = Default.class, to = DriverChecks.class)
private Driver driver;
public Car(String manufacturer, String licencePlate, int seatCount) {
this.manufacturer = manufacturer;
this.licensePlate = licencePlate;
this.seatCount = seatCount;
}
// getters and setters ...
}
As a result the validation in Example 5.11, “Test case for @ConvertGroup” succeeds, even though the
constraint on hasDrivingLicense belongs to the
DriverChecks
group and only the
Default
group is requested in the
validate()
call.
Example 5.11. Test case for @ConvertGroup
// create a car and validate. The Driver is still null and does not get validated
Car car = new Car( "VW", "USD-123", 4 );
car.setPassedVehicleInspection( true );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );
// create a driver who has not passed the driving test
Driver john = new Driver( "John Doe" );
john.setAge( 18 );
// now let's add a driver to the car
car.setDriver( john );
constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals(
"The driver constraint should also be validated as part of the default group",
constraintViolations.iterator().next().getMessage(),
"You first have to pass the driving test"
);
You can define group conversions wherever
@Valid
can be used, namely associations as well as
method and constructor parameters and return values. Multiple conversions
can be specified using @ConvertGroup.List
.
However, the following restrictions apply:
@ConvertGroup
must only be used in
combination with @Valid
. If used without, a
ConstraintDeclarationException
is
thrown.
It is not legal to have multiple conversion rules on the same
element with the same from
value. In this
case, a ConstraintDeclarationException
is
raised.
The from
attribute must not refer to a
group sequence. A
ConstraintDeclarationException
is raised in
this situation.
Rules are not executed recursively. The first matching
conversion rule is used and subsequent rules are ignored. For example
if a set of @ConvertGroup
declarations chains
group A to B and B to C, the group A will be converted to B and not to
C.
Copyright © 2009 - 2013 Red Hat, Inc. & Gunnar Morling