Bean Validation 1.1: method validation!


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

Advertisements

2 thoughts on “Bean Validation 1.1: method validation!

  1. Gelin Luo

    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)

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s