A great enhancement added to Bean Validation in version 1.1 was the “famous” Appendix C of the 1.0 specification: the method validation.
This one allows you to validate method parameters and method returned values.
To illustrate this article i’ll simply take a simple bean handling a name with some constraints:
public class ImportantBean { private String name; public ImportantBean(final String name) { this.name = name; } public String updateName(final String firstName, final String lastName) { name = firstName + " " + lastName; return name; } }
The constraints on this bean are:
- The name can never be null
- When updating a name first and last names should have at least a length of 1 and both can’t be the same (You can’t be called Jean Jean)
So very intuitively you’ll decorate your bean:
public class ImportantBean { private String name; public ImportantBean(final @NotNull String name) { this.name = name; } @FirstnameDifferentFromLastname // cross parameters, see associated validator @Size(min = 3) // on return value public String updateName(final @NotNull @Size(min = 1) String firstName, final @NotNull @Size(min = 1) String lastName) { name = firstName + " " + lastName; return name; } }
Validating parameter looks quite obvious, validating returned value too but what about constraints between parameters? This is the role of @FirstnameDifferentFromLastname in our sample.
@FirstnameDifferentFromLastname is a custom annotation associated with a custom validator which simply ensure two parameters are different. Here are the code for both:
@Constraint(validatedBy = { NameValidator.class }) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD }) public @interface FirstnameDifferentFromLastname { Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String message() default "{com.github.rmannibucau.bv.methodvalidation.FirstnameDifferentFromLastname.message}"; }
@SupportedValidationTarget(value = ValidationTarget.PARAMETERS) public class NameValidator implements ConstraintValidator<FirstnameDifferentFromLastname, Object[]> { @Override public void initialize(final FirstnameDifferentFromLastname constraintAnnotation) { // no-op } @Override public boolean isValid(final Object[] parameters, final ConstraintValidatorContext context) { return parameters == null || (parameters.length == 2 && !parameters[0].equals(parameters[1])); } }
You probably already seen the magic: @SupportedValidationTarget. It defines a constraint on a method as either a constraint on the returned value or on parameters – our case – (it is called a cross parameter constraint since it applies on parameters array).
So ok, we have our bean, this one has its constraints but how is it validated?
There are several cases:
- you are in CDI: by default constructors and non getter methods are validated so nothing to do (you can configure to validate everything, nothing, just constructors….)
- you are not in CDI or the method/constructor validation is not enabled
If you are in the second case you’ll have to get the Validator and get from it an ExecutableValidator. To do so you have the method forExecutables():
final ExecutableValidator executableValidator = validator.forExecutables();
Then you have validateParameters() and validateReturnValue() methods (using a method or a constructor) to get the set of violations for the passed parameters.
Here is an arquillian test using these methods to validate our bean manually:
@RunWith(Arquillian.class) public class MethodValidationTest { @Deployment public static Archive<?> war() { return ShrinkWrap.create(WebArchive.class, "method-validation.war") .addPackage(ImportantBean.class.getPackage()) .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); } @Inject private Validator validator; @Test public void validateMethodParameters() throws Exception { final ImportantBean bean = new ImportantBean("name"); final Method method = ImportantBean.class.getMethod("updateName", String.class, String.class); final ExecutableValidator executableValidator = validator.forExecutables(); final Set<ConstraintViolation<ImportantBean>> violations = executableValidator.validateParameters(bean, method, new Object[]{ "a", "a" }); assertEquals(1, violations.size()); } @Test public void validateMethodReturnedValue() throws Exception { final ImportantBean bean = new ImportantBean("name"); final Method method = ImportantBean.class.getMethod("updateName", String.class, String.class); final ExecutableValidator executableValidator = validator.forExecutables(); final Set<ConstraintViolation<ImportantBean>> violations = executableValidator.validateReturnValue(bean, method, ""); // too short assertEquals(1, violations.size()); } @Test public void validateConstructorParameters() throws Exception { final Constructor<ImportantBean> constructor = ImportantBean.class.getConstructor(String.class); final ExecutableValidator executableValidator = validator.forExecutables(); final Set<ConstraintViolation<ImportantBean>> violations = executableValidator.validateConstructorParameters(constructor, new Object[]{null}); assertEquals(1, violations.size()); } }
Note: to activate or not by exception the validation in CDI (= specifically on a method/class) you can use @ValidateOnExecution annotation
I found a problem with ExecutableValidator.validateParameters API: once it returns a set of violations, you can’t link the violation in the set to specific parameter (by index)
Maybe open a ticket on the spec bugtracker, think it is a very good feedback.