May 3, 2015

JSF: Validation Localization (I18N) by example (part 2 of 3)


Pages: 1 2 3

Validation and conversion with I18N – continued

Bean Validation: Overwrite individual I18N messages

It’s rather easy to localize a single Bean Validation constraint. This typically applies when you use a very general constraint annotation (such as @Pattern) to describe a very local business validation constraint.

In this case, you’ll have to set the message attribute of the constraint annotation. Set the message in angle brackets so it gets interpreted as a ValidationMessage key and not as plain hard-coded text:
@Pattern(regexp = "[A-Za-z\\. ]+", message = "{model.book.author}") private String author;
and define in your own ValidationMessages.properties:
model.book.author=must be a proper author name (no diacritics)
Then, if in the GUI, the user enters an illegal “author”, he gets the validation error
Author: must be a proper author name (no diacritics)
(The regex pattern used in this example is very rudimentary and wouldn’t match real author names).

JSF validators: the fallback

Actually, many BeanValidation validators have a JSF validator component equivalent. For example, implementing above Regex pattern validator without Bean Validation, but with vanilla JSF validation is as simple as:
<p:inputText id="author" value="#{bookController.item.author}" required="true">
    <f:validateRegex pattern="[A-Za-z\\. ]+" />
</p:inputText>
However, I do strongly recommend seeing JSF validators as a “last resort” solution only (except for the required flag), for the pro-Bean Validation reasons I mentioned earlier. Freely mixing JSF validation and Bean Validation will harm the overall application architecture.

I will thus not cover any JSF validator-related discussions here.

Conversion errors

It’s a common use case to convert the value a user entered into the value the backing bean expects (this by the way is implicitly the case with all non-String backing bean values). Here, JSF components shine, namely the JSF converter components.

For example, if you want to convert a four-digit year input into its “01/01/year” java.util.Date equivalent, you can use the appropriate JSF converter for that:
<p:inputText id="releaseYear" value="#{bookController.item.releaseYear}">
    <f:convertDateTime pattern="yyyy" />
</p:inputText>
But of course, converters will also trigger converter error messages.

If in the GUI, the user enters an illegal “year”, he gets a converter error such as
Release year: 'asdf' could not be understood as a date.
If you want to overwrite the default converter error message, again you search your JSF implementation’s default messages for the appropriate key and overwrite it in your own ValidationMessages.properties:
javax.faces.converter.DateTimeConverter.DATE={2}: must be a four-digit year
Note again the discrepancies between many of the default messages: Some end with a period (.), some don’t. There are also three forms of the placeholder pattern:
  • For the onefold placeholder patterns: {0} is the component reference.
  • For the twofold placeholder patterns: {0} is the component’s submitted value and {1} is the component reference.
  • For the threefold placeholder patterns: {0} is the component’s submitted value, {1} is a hard-coded example of a valid value, and {2} is the component reference.
This indeed is very annoying, and you have to pay close attention to this if you specify your own localization messages. It will typically also force you to re-write most of the default messages just to unify usage of end-of-sentence period and to get rid of the “example” values which are typically of no use for the end-user.

After you eventually completed javax.faces.converter.DateTimeConverter.DATE customization: if in the GUI, the user enters an illegal “year”, he now gets the converter error
Release year: must be a four-digit year

Conversion errors: Overwrite individual I18N messages

Even though we just did that for the sake of the previous example, it would be rather silly to actually override the global javax.faces.converter.DateTimeConverter.DATE message with “must be a four-digit year”, you’d rather specify an individual message for this special case.

You could either switch to a Java-based @FacesConverter implementation where you would retrieve the appropriate converter message programmatically, or you can simply stick with JSF in-page code by specifying the converterMessage attribute, e.g. as follows:
<o:importFunctions type="org.omnifaces.util.Faces"/>
<p:inputText id="releaseYear" value="#{bookController.item.releaseYear}" 
    converterMessage="#{of:format1(Faces:getMessageBundle().getString('model.book.releaseYear.date'), component.label)}">
    <f:convertDateTime pattern="yyyy" />
</p:inputText>
Note that this makes heavy use of some OmniFaces util functionality (it’s best practice not to invent the wheel twice). Learn more about <o:importFunctions> and the Faces util class in the OmniFaces showcase. All that code does is basically retrieve the converter message in ValidationMessages.properties rather than in the messages.properties resource bundle. Also note that through the implicit #{component} EL object, we can retrieve its label implicitly. As I reused the same logic later on, I even went one step further and created a static helper method in the Validation class which will further compact the code:
<o:importFunctions type="ch.codebulb.jsfvalidationlocalization.util.Validation" var="val"/>
<p:inputMask id="releaseYear" value="#{bookController.item.releaseYear}"
             converterMessage="#{val:labeledMsg(component, 'model.book.releaseYear.date')}"
             title="#{msg['model.book.releaseYear.help']}"
             mask="9999">
    <f:convertDateTime pattern="yyyy" />
</p:inputMask>
Now you can add the custom message model.book.releaseYear.date to ValidationMessages.properties as usual:
model.book.releaseYear.date={0}: must be a four-digit year
If in the GUI, the user then enters an illegal “year”, he gets a converter error such as
Release year: must be a four-digit year
again.

Implicit conversion errors

As mentioned earlier, some JSF components implicitly add a converter to convert the submitted / received value. For example, PrimeFaces’ <p:calendar> component implicitly adds a date converter. The same is true when using primitives in the backing bean. For example,
private int pages;
will implicitly add an IntegerConverter – which in turn will throw a conversion error if the user enters illegal input.

So if in the GUI, the user enters an illegal “page”, he gets a converter error such as
Pages: 'asdf' must be a number consisting of one or more digits.
Thus do not forget to test conversion errors as well! You typically want to overwrite default localization for these conversion errors, too. Again, look up the respective message key in your JSF implementation and set it appropriately:
javax.faces.converter.IntegerConverter.INTEGER={2}: must be a number consisting of one or more digits
Now if in the GUI, the user enters an illegal “page”, he gets a converter error such as
Pages: must be a number consisting of one or more digits

Custom / cross-field validation

Sometimes, you’ll have to implement validation logic which is not covered by the rather simple default Bean Validation constraints. First of all, check whether there is actually no existing constraint covering your requirements:
There are actually four ways to implement your own validator:
  • Bean Validation based: Implement your own Bean Validation ConstraintValidator + annotation
  • Bean Validation based: Use the somewhat hackish @AssertTrue annotation
  • JSF validation based: Implement your own @FacesValidator
  • JSF + OmniFaces based: MultiFieldValidator
At first glance, one would prefer a Bean Validation based approach to keep the overall architecture consistent, with Bean Validation being the first choice so far for any of your validation needs.

The @FacesValidator approach has its own severe limitation: It is of no use for cross-field-validation as it only accepts one JSF component’s value as input parameter. (Yes, there are some hacky ways to achieve cross-field support, but they don’t really match the premise of “best practices” application.) Either way, cross-field validation is a quite typical use case of a custom validation constraint: That’s when two or more bean properties depend on the value of each other. To see a @FacesValidator implementation in action, please head to the next section.

Finally, the OmniFaces library offers special support for cross-field validation, at the cost of introducing a somewhat proprietary JSF extension for basic validation needs.

Thus, let’s first examine a Bean Validation based approach. They are both described in this stackoverflow thread. They share basically the same advantages and disadvantages:
  • They have the previously discussed advantages of Bean Validation constraints (over GUI-Layer-only validation)
  • They are not part of JSF’s validation lifecycle and will thus either be called by entity manager #persist(…) (if present), or the bean validation must be explicitly called in the backing bean. This has the major disadvantage that all other validations first have to pass until this custom validation constraint gets evaluated. This will disrupt the user experience.
In this situation, I would prefer the more lean, KISS way which avoids defining a custom annotation as is works with @AssertTrue.

As a concrete example, I’d now like to cover this requirement: “Either both the book’s release year and its edition must be provided, or none of them.” This is a rather typical cross-field validation.

I would implement it with @AssertTrue like so:
@AssertTrue(message = "{model.book.releaseYearAndEditionPresent}")
public boolean isReleaseYearAndEditionPresent() {
    return (getReleaseYear() == null && getEdition() == 0) ||
            (getReleaseYear() != null && getEdition() != 0);
}
Then don’t forget to fire validation explicitly in the backing bean action (omit this step if validation gets fired implicitly, e.g. alongside entity manager’s #persist(…) call):
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Book>> violations = validator.validate(item);
for (ConstraintViolation<Book> violation : violations) {
    Messages.addGlobalError(violation.getMessage());
}
Let’s compare this with the OmniFaces validation support.

This JSF library offers a couple of brilliant JSF validators. Their simplicity is a good reason to opt for a pure JSF implementation in this case. Also, they are implicitly part of the JSF validation lifecycle without the need to call them explicitly. The user experience stays smooth and consistent.

<o:validateMultiple> is the most powerful of OmniFaces’ JSF validators as it’ll match literally any custom / cross field validator requirement. This is the XHTML code for above validation rule:
<o:validateMultiple id="releaseYearAndEditionPresent" components="releaseYear edition" 
    validator="#{bookController.isReleaseYearAndEditionPresent}"
    message="#{'{0}: '.concat(val:msg('model.book.releaseYearAndEditionPresent'))}"/>
You simply pass in the id of the input components you want to have validated, and then implement the validator function. I would actually advice to go for a truly domain-driven implementation, i.e. to rebuild (parts of) the true domain model, fill in its values, and then invoke a validator method. That way, the validation logic is still kept inside the model, in line with existing Bean Validation logic. However, this means writing some initial boilerplate code as you’ll have to manually care for conversion in that custom method. Here's the implementation:
public boolean isReleaseYearAndEditionPresent(FacesContext context, List<UIInput> components, List<Object> values) {
    Book book = new Book();
    book.setReleaseYear((Date) values.get(0));
    try {
        book.setEdition(Integer.parseInt(values.get(1).toString()));
    }
    catch (NumberFormatException ex) {}
    return book.isReleaseYearAndEditionPresent();
}

Actually in this very use case (“Fill in all or none of these input fields”) you could just have used one of the other predefined validators of OmniFaces: <o:validateOneOrNone>, which does exactly that, without the need to implement any custom validation logic. This is the XHTML code for above validation rule, this time with <o:validateOneOrNone>:
<o:validateOneOrNone id="releaseYearAndEditionPresent" components="releaseYear edition" 
    message="#{'{0}: '.concat(val:msg('model.book.releaseYearAndEditionPresent'))}"/>
No Java code is required.
Considering ease of development and JSF integration, I'd thus opt for an OmniFaces based cross-field-validation.

Service-based validation

So far we only covered validation logic based on a local model’s values. But sometimes you need to check these values against a service.

For example, let’s assume we’d like to check whether the entered book title is on a “blacklist” and should thus be revoked.

The most simple implementation of course is to just implement programmatic validation in a controller’s method where you have full access to any injected services. However, this method call would not be part of the JSF / Bean Validation lifecycle and clutter validation logic all over the application. This is not the way to go.

The next sensible idea would be to implement validation in a @FacesValidator, but unfortunately, this component is not eligible for dependency injection. As BalusC explained in his stackoverflow answer, there are currently two workarounds:
  • If you have OmniFaces + CDI (Context and Dependency Injection from the Java EE stack), @FacesValidator is implicitly modified to support dependency injection.
  • Otherwise, use the workaround explained in the stackoverflow answer.
I assume again that any decent JSF tech stack features OmniFaces, thus I opt for this solution.

Implementing the validator is pretty straightforward. Thanks to OmniFaces, CDI injection works perfectly.
@FacesValidator
public class BookTitleValidator implements Validator {
    @Inject
    BookService service;

    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        String input = (String) value;
        
        if (service.isInBannedTitles(input)) {
            throw Validation.createValidatorException(component, "model.book.title.isInBannedTitles");
        }
    }
}
Adding the validator to the input component is simple as well:
<p:inputText id="title" value="#{bookController.item.title}" required="true" maxlength="100"
    validator="bookTitleValidator"/>
However, there’s a pitfall: The validator’s error message is not based on the javax.faces.validator.BeanValidator.MESSAGE format (as are none of the javax.faces.validator.* messages). As a consequence, the validator error message lacks the famous label even though it is well aware of the component which caused the error. Not again!

Thus if in the GUI, the user enters an illegal “title”, e.g. “Peter Pan”, he gets the validator error
this book title is blacklisted and thus not allowed
No label! However, fixing this issue is simple. You just have to format the output message accordingly in your Java code. I would recommend that for DRY reasons, you do this
  • in a static helper method; and
  • based on retrieving the javax.faces.validator.BeanValidator.MESSAGE key
For example, as in the demo application's Validation class.

Now if in the GUI, the user enters an illegal “title”, e.g. “Peter Pan”, he gets the validator error
Title: this book title is blacklisted and thus not allowed
Also note that this custom validator is part of JSF’s validator lifecycle, and its validator error message is displayed alongside all other „local“ error messages. Of course, if your validation includes invocation of a true remote service, I’d recommend to rather do this check programmatically in the submit action, independent of „true“ validation logic. Remote services should only be invoked after all basic „local“ validations passed. For more sophisticated cases, a business rule engine could be introduced.

Pages: 1 2 3

No comments: