Tuesday, February 16, 2010

Bookmark and Share

One question I'm being asked pretty often when talking about the Bean Validation API (JSR 303) is, how cross-field validation can be done. The canonical example for this is a class representing a calendar event, where the end date of an event shall always be later than the start date.

This and other scenarios where validation depends on the values of multiple attributes of a given object can be realized by implementing a class-level constraint.

That basically works pretty well, nevertheless this is one part of the BV spec, I'm not too comfortable with. This is mainly for two reasons:

  • You'll probably need a dedicated constraint for every reasonably complex business object. Providing an annotation, a validator implementation and an error message for each can become pretty tedious.
  • Business objects typically know best themselves, whether they are in a consistent, valid state or not. By putting validation logic into a separate constraint validator class, objects have to expose their internal state, which otherwise might not be required.

To circumvent these problems, I suggested a while ago a generic constraint annotation, which allows the use of script expressions written in languages such as Groovy to implement validation logic directly at the validated class.

This approach frees you from the need to implement dedicated constraints for each relevant business object, but also comes at the cost of losing type-safety at compile-time.

The constraint presented in the following therefore tries to combine genericity with compile-time type safety. The basic idea is to implement validation logic within the business objects themselves and to invoke this logic from within a generic constraint class.

In order to do so let's first define an interface to be implemented by any validatable class:

1
2
3
4
public interface Validatable {

    public boolean isValid();
}

Next we define a constraint annotation, @SelfValidating, which we'll use later on to annotate Validatable implementations:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target( { TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = SelfValidatingValidator.class)
@Documented
public @interface SelfValidating {

    String message() default "{de.gmorling.moapa.self_validating.SelfValidating.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

Of course we also need a constraint validator implementation, which is able to evaluate that constraint:

1
2
3
4
5
6
7
8
9
10
11
public class SelfValidatingValidator implements
    ConstraintValidator<SelfValidating, Validatable> {

    public void initialize(SelfValidating constraintAnnotation) {}

    public boolean isValid(Validatable value,
        ConstraintValidatorContext constraintValidatorContext) {

        return value.isValid();
    }
}

The implementation is trivial, as nothing is to do in initialize() and the isValid() method just delegates the call to the validatable object itself.

Finally we need a properties file named ValidationMessages.properties containing the default error message for the constraint:

1
de.gmorling.moapa.self_validating.SelfValidating.message=Validatable object couldn't be validated successfully.

Taking the calendar event example from the beginning, usage of the constraint might look as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@SelfValidating
public class CalendarEvent implements Validatable {

    @NotNull
    private final String title;

    private final Date startDate;

    private final Date endDate;

    public CalendarEvent(String title, Date startDate, Date endDate) {

        this.title = title;
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public boolean isValid() {
        return startDate == null || endDate == null || startDate.before(endDate);
    }

    @Override
    public String toString() {
        DateFormat format = new SimpleDateFormat("dd.MM.yyyy");

        return 
            title + 
            " from " +  (startDate == null ? "-" : format.format(startDate)) + 
            " till " + (endDate == null ? "-" : format.format(endDate));
    }

}

A short unit test shows that the constraint works as expected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SelfValidatingTest {

    private static Validator validator;

    private static Date startDate;

    private static Date endDate;

    @BeforeClass
    public static void setUpValidatorAndDates() {

        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();

        startDate = new GregorianCalendar(2009, 8, 20).getTime();
        endDate = new GregorianCalendar(2009, 8, 21).getTime();
    }

    @Test
    public void calendarEventIsValidAsEndDateIsAfterStartDate() {

        CalendarEvent testEvent = 
            new CalendarEvent("Team meeting", startDate, endDate);

        assertTrue(validator.validate(testEvent).isEmpty());
    }

    @Test
    public void calendarEventIsInvalidAsEndDateIsBeforeStartDate() {

        CalendarEvent testEvent = 
            new CalendarEvent("Team meeting", endDate, startDate);

        Set<ConstraintViolation<CalendarEvent>> constraintViolations = 
            validator.validate(testEvent);
        assertEquals(1, constraintViolations.size());

        assertEquals(
            "Object couldn't be validated successfully.",
            constraintViolations.iterator().next().getMessage());
    }
}

This works like a charm, only the error message returned by the BV runtime is not yet very expressive. This can easily be solved, as the BV API allows to override error messages within constraint annotations:

1
2
3
4
@SelfValidating(message="{de.gmorling.moapa.self_validating.CalendarEvent.message}")
public class CalendarEvent implements Validatable {
    ...
}

Again we need an entry in ValidationMessages.properties for the specified message key:

1
de.gmorling.moapa.self_validating.CalendarEvent.message=End date of event must be after start date.

Conclusion

Providing dedicated class-level constraints for all business objects that require some sort of cross-field validation logic can be quite a tedious task as you need to create an annotation type, a validator implementation and an error message text.

The @SelfValidating constraint reduces the required work by letting business objects validate themselves. That way all you have to do is implement the Validatable interface, annotate your class with @SelfValidating and optionally provide a customized error message. Furthermore business objects are not required to expose their internal state to make it accessible for an external validator implementation.

The complete source code can be found in my GitHub repository. As always I'd be happy about any feedback.

14 comments:

Rommel Pino said...

Hi Gunnar!
This idea is great. I just wonder the following things:
1)What will happen if I will put a @SelfValidating annotation on a class that doesn't implement a Validatable interface? If this will not throw a compile-time error then better not to enforce implementation of Validatable interface but just invoke the isValid() method on that object through java reflection(note - this can be achieve in my case as just placing a plain @AssertMethodAsTrue annotation with no parameters on my class because the default method that it will invoke is isValid()).

2)What if I have three business logic that needs to be validated on this class, of course I could call them all inside the isValid() method, but what if just one of them fails, how can I display a specific message on the failing logic?

If this concerns will be resolved, then I could say that this @SelfValidating annotation will be a must-have.

regards,
Pino

Unknown said...

Hi Pino,

great to hear you like the idea. To answer your questions:

1) Generally it's not possible to restrict the types at which a given annotation may be specified, you only can specify the allowed element kinds (types, methods, fields etc.).

What one can do though is to provide an annotation processor, that hooks into the compiler and raises an error upon compilation, if a constraint annotation is given at an unsupported type. Actually I'm building such a processor right now for Hibernate Validator.

2) Without knowing the exact requirements, I probably would work with a specific constraint in that case.

Within the validator implementation the ConstraintValidatorContext can be used to create custom constraint violations depending on your specific rules.

Alternatively you might create a complex constraint composed from multiple constraints representing the different rules.

Gunnar

Unknown said...

Hi Gunnar
Great articles, they've helped us a lot in getting validation up and running.

We are trying to create a class-level constraint for checking that entities are unique. For some strange reason Hibernate doesn't do this before submitting to the database (even though @UniqueConstraint's are in place). This UniqueValidator needs access to the EntityManager. Any idea on how to get access to it?

We've tried with @PersistenceContext and using Persistence.createEntityManager(".."), in the first case we get null and in the second we get an exception:
"The chosen transaction strategy requires access to the JTA TransactionManager".

We suspect it has something to do with how the Validator is instantiated, but we're a bit inexperienced with JPA and JCDI.

Any suggestions?

Regards, Anders

Unknown said...

Anders,

it's hard to say what the actual problem is without knowing the project itself.

What concerns retrieving additional dependencies within validator implementations, you could either use JNDI lookups (have a look at the JPA 2 spec chapter 7.2 how to lookup an EntityManager within a Java EE container) or you could provide a custom ConstraintValidatorFactory which performs DI on the validator instances it creates.

Anonymous said...

Hi,

I've stumbled upon this by chance and I am really impressed with the idea and the implementation even though the points made out by Pino are true for my needs.

Unknown said...

Anonymous,

I got to admit that I'm not really convinced of the idea anymore.

A much simpler approach would just be

class MyBean {

  @AssertTrue
  public boolean isValid() {
    //validation logic
  }

}

Ollie said...

Seriously, what's wrong with having the assertions right in the constructor. This has a series of advantages:

First, you can get rid of all the annotation and framework madness and shrink down the code to the actual business code. Second, you cannot instantiate the object in an invalid state. Especially the latter is extremely valuable as one can be sure the object is a valid state if one gets one returned from a method or handed into a method. With the shown approach you always have to rely on someone call the isValid() method and can never be sure someone already did. So you'll probably end up with unnecessary calls to that method "just to be sure".

Simon said...

Yes, I much prefer using the @AssertTrue validation for this - partly because it's standard (and so doesn't require me to maintain code to do it), and partly because you can use it more than once. That's important, because you might have several multi-field validations you need to run, each with different messages.

Anonymous said...

A more elegant solution here :
http://soadev.blogspot.fr/2010/01/jsr-303-bean-validation.html

1croreprojects said...



Thanks for sharing this valuable information.
java projects in chennai
dotnet projects in chennai
ns2 projects in chennai

stewe said...

The thing with @AssertTrue is that I lose the ability to add my custom constraint violation messages when cross parameter validation fails. With @AssertTrue, I just have the info that it failed, but I dont know why. This can be done via custom validator as shown initially in this article.

MeriFain said...

This was a fantastically written post with a straightforward description. That is why I am also providing you with my article. The post is based on a keyboard tester tools , and I discovered some popular 9 simple online keyboard tester websites to help you test laptop keyboards online quickly and easily.

Sruthi Karan said...

Thank you so much for your excellent blog! I really enjoy to visit your very interesting post, Well done!
Best Family Court Lawyers Near Me
Best Female Family Law Attorney

Anonymous said...

ما هي طريقه فتح محفظه في الاسهم؟
تعد محفظة اسهم من أهم المحافظ الاستثمارية على الإطلاق، حيث تضم جميع استثمارات العميل