Chapter 11. Client-Side Validation API (JSR 303)

Chapter 11. Client-Side Validation API (JSR 303)

11.1. Getting Started with the GraniteDS Validation Framework
11.2. Working with Error Messages and Localization
11.3. Working with Groups
11.4. Integration with Code Generation Tools (Gas3)
11.5. Writing your own Constraints
11.6. Using the FormValidator Class
11.7. Notes on Compatibility

The "Bean Validation" (aka JSR-303) standardizes an annotation-based validation framework for Java. It provides an easy and powerful way of processing bean validations, with a pre-defined set of constraint annotations, allowing to arbitrarily extend the framework with user specific constraints.

Flex doesn't provide by itself such a framework. The standard way of processing validation is to use subclasses and to bind a validator to each user input (see ). This method is at least time consuming for the developer, source of inconsistencies between the client-side and the server-side validation processes, and source of redundancies in your MXML code.

Starting with the release 2.2, GraniteDS introduces an ActionsScript3 implementation of the Bean Validation specification and provides code generation tools integration so that your Java constraint annotations are reproduced in your AS3 beans.

As its Java equivalent, the GraniteDS validation framework provides a set of standard constraints. Here is an overview of these constraints (see for details):

ConstraintDescription
AssertFalseThe annotated element must be false
AssertTrueThe annotated element must be true
DecimalMaxThe annotated element must be a number whose value must be lower or equal to the specified maximum
DecimalMinThe annotated element must be a number whose value must be greater or equal to the specified minimum
DigitsThe annotated element must be a number within accepted range
FutureThe annotated element must be a date in the future
MaxThe annotated element must be a number whose value must be lower or equal to the specified maximum
MinThe annotated element must be a number whose value must be greater or equal to the specified minimum
NotNullThe annotated element must not be null
NullThe annotated element must be null
PastThe annotated element must be a date in the past
PatternThe annotated String must match the specified regular expression
SizeThe annotated element size must be between the specified boundaries (included)

Each of these contraint annotation may be applied on a bean property, depending on its type and its expected value:

Annotated AS3 Bean Properties:

public class MyAnnotatedBean {

    [NotNull] [Size(min="2", max="8")]
    public var name:String;

    private var _description:String;

    [Size(max="255")]
    public function get description():String {
        return _description;
    }
    public function set description(value:String) {
        _description = value;
    }
}
        

In the above code sample, the name value must not be null and its length must be between 2 and 8 characters, and the description value may be null or may have a length of maximum 255 characters. Constraint annotations must be placed on public properties, either public variables or public accessors (and they may also be placed on the class itself).

In order to validate an instance of the above class, you may use the ValidatorFactory class.

import org.granite.validation.ValidatorFactory;
import org.granite.validation.ConstraintViolation;

var bean:MyAnnotatedBean = new MyAnnotatedBean();

var violations:Array = ValidatorFactory.getInstance().validate(bean);
trace((violations[0] as ConstraintViolation).message); // "may not be null"

bean.name = "123456789";
violations = ValidatorFactory.getInstance().validate(bean);
trace((violations[0] as ConstraintViolation).message); // "size must be between 2 and 8"

bean.name = "1234";
violations = ValidatorFactory.getInstance().validate(bean);
trace(violations.length); // none...
        

Validation may be much more complex than the above basic sample. GraniteDS validation framework supports all advanced concepts of the specification, such as groups, group sequences, default group redefinition, traversable resolver, message interpolator, etc. Please refer to the specification and the various tutorials you may find on the Net.

Tip

Compilation Tip: You must use the compiler option -keep-as3-metadata+=AssertFalse,AssertTrue,DecimalMax,DecimalMin, Digits,Future,Max,Min,NotNull,Null,Past,Pattern,Size or the corresponding configuration for your build system (see Project Setup for Ant and Maven) in order to tell the Flex compiler to keep the constraint annotations in your compiled code (Flash Builder 4 appears to keep all metadata by default, but the mxmlc command line compiler doesn't)! If you write your own constraints, you will also have to tell the compiler about them in the same way.

Default error messages for built-in constraints are provided in four languages: english, french, german (as in the javax API distribution) and chinese. Depending on the current locales specified in the ResourceManager.getInstance().localeChain array, error messages will be localized in one of these languages (defaulted to english if you use other locales).

The easiest way to customize error messages is to use the message attribute of the constraint annotation:

public class MyBean {

    [NotNull(message="Name is mandatory"]
    [Size(min="2", message="Name must have a length of at least {min} characters")]
    public var name;

    ...
}
        

As you can see, you may use parameters (the min attribute) in such customized messages. These error messages are much more accurate than the default ones ("may not be null", "size must be between..."), but you must specify them for each constraint and you cannot localize the literals used for multiple languages.

In order to add support for different locales, you will have to define variables (eg. name.notnull and name.minsize) and use the built-in support offered by Flex:

public class MyBean {

    [NotNull(message="{name.notnull}"]
    [Size(min="2", message="{name.minsize}")]
    public var name;

    ...
}
        

locale/en_US/ValidationMessages.properties

name.notnull=Name is mandatory
name.minsize=Name must have a length of at least {min} characters
        

locale/fr_FR/ValidationMessages.properties

name.notnull=Le nom est obligatoire
name.minsize=Le nom doit avoir une taille d'au moins {min} caractères
        

Register your Bundles:

[ResourceBundle("ValidationMessages")]
        

If you compile your Flex application with support for these two locales (see Flex ), the error messages will be localized in english or french, depending on the current selected locale, with the values set in your property files. You may also redefine standard messages for a given locale in the same way:

locale/en_US/ValidationMessages.properties

name.notnull=Name is mandatory
name.minsize=Name must have a length of at least {min} characters
javax.validation.constraints.NotNull.message=This value is mandatory
        

With the above bundle, the default error message for the NotNull constraint and the locale en_US will be redefined to "This value is mandatory" (instead of "may not be null").

Adding support for one or more locales other than the default ones will follow the same principle: create a ValidationMessages.properties for the new locale, translate all default error messages and add new ones for your customized message keys. Note that the bundle name must always be set to "ValidationMessages".

As stated by the specification (section 3.4): “ A group defines a subset of constraints. Instead of validating all constraints for a given object graph, only a subset is validated. This subset is defined by the the group or groups targeted. Each constraint declaration defines the list of groups it belongs to. If no group is explicitly declared, a constraint belongs to the Default group.

The GraniteDS validation framework fully supports the concepts of group, group inheritance, group sequence, default group redefinition and implicit grouping. Like in Java, groups are represented by interfaces. For example, suppose that you want to define and use a path.to.MyGroup group. You will have to write the interface, to reference it in some of your constraints and to call the ValidatorFactory.validate method with one extra parameter:

package path.to {
    public interface MyGroup {}
}
...

public class MyBean {

    [NotNull]
    [Size(min="2", max="10", goups="path.to.MyGroup")]
    public var name;

    ...
}
...

var bean:MyBean = new MyBean();

// Default group: NotNull fails.
ValidatorFactory.getInstance().validate(bean);

// MyGroup group: no failure.
ValidatorFactory.getInstance().validate(bean, [MyGroup]);

// Default & MyGroup groups: NotNull fails.
ValidatorFactory.getInstance().validate(bean, [Default, MyGroup]);

bean.name = "a";

// Default group: no failure.
ValidatorFactory.validate(bean);

// MyGroup group: Size fails.
ValidatorFactory.getInstance().validate(bean, [MyGroup]);

// Default & MyGroup groups: Size fails.
ValidatorFactory.getInstance().validate(bean, [Default, MyGroup]);
        

You may of course specify mutliple groups in the constraint annotation, for example [Size(min="2", max="10", goups="path.to.MyGroup, path.to.MyOtherGroup")]. Because the group interface references in the annotations must be fully qualified, it may be annoying to always specify the complete path to each group interface, and you may use the namespace resolver available in the ValidatorFactory instance:

ValidatorFactory.getInstance().namespaceResolver.registerNamespace("g", "path.to.*");
...

[Size(min="2", max="10", goups="g:MyGroup, g:MyOtherGroup")]
public var name;
        

Note that the group interface is always registered in the default namespace and may be use without any prefix specification: groups="Default" is legal and strictly equivalent to groups="org.granite.validation.groups.Default" (or even groups="javax.validation.groups.Default" - as the javax package is handled as an alias of the granite's one).

The Bean Validation specification is primarily intended to be used with Java entity beans. GraniteDS code generation tools replicate your Java model into an ActionScript 3 model and may be configured in order to copy validation annotations. All you have to do is to change the default org.granite.generator.as3.DefaultEntityFactory to org.granite.generator.as3.BVEntityFactory.

With the Eclipse builder, go to the "Options" panel and change the entity factory as shown is the picture below:

With the Ant task, use the entityfactory attribute as follow in your build.xml:



<gas3 entityfactory="org.granite.generator.as3.BVEntityFactory" ...>
    ...
</gas3>
        

Then, provided that you have a Java entity bean like this one:



@Entity
public class Person {
    @Id @GeneratedValue
    private Integer id;
    
    @Basic
    @Size(min=1, max=50)
    private String firstname;
    
    @Basic
    @NotNull(message="You must provide a lastname")
    @Size(min=1, max=255)
    private String lastname;
    // getters and setters...
}
        

... you will get this generated ActionScript3 code:

[Bindable]
public class PersonBase implements IExternalizable {

    ...

    public function set firstname(value:String):void {
        _firstname = value;
    }
    [Size(min="1", max="50", message="{javax.validation.constraints.Size.message}")]
    public function get firstname():String {
        return _firstname;
    }

    public function set lastname(value:String):void {
        _lastname = value;
    }
    [NotNull(message="You must provide a lastname")]
    [Size(min="1", max="255", message="{javax.validation.constraints.Size.message}")]
    public function get lastname():String {
        return _lastname;
    }

    ....
}
        

You may then use the ValidationFactory in order to validate your ActionScript 3 bean, and the same constraints will be applied on the Flex and the Java sides.

This works for plain Java beans and entity beans.

Suppose you want to make sure that a Person bean has at least one of its firstname or lastname properties not null. There is no default constraint that will let you check this. In order to implement a constraint that will do this validation, you will have to write a new IConstraint implementation, register it with the ValidatorFactory and use the corresponding annotation on top of the Person class.

PersonChecker.as

public class PersonChecker extends BaseConstraint {

    override public function initialize(annotation:Annotation, factory:ValidatorFactory):void {
        // initialize the BaseContraint with the default message (a bundle key).
        internalInitialize(factory, annotation, "{personChecker.message}");
    }

    override public function validate(value:*):String {
        // don't validate null Person beans.
        if (Null.isNull(value))
            return null;

        // check value type (use helper class).
        ConstraintHelper.checkValueType(this, value, [Person]);

        // validate the Person bean: at least one of the firstname or lastname property
        // must be not null.
        if (Person(value).firstname == null && Person(value).lastname == null)
            return message;

        // return null if validation is successful.
        return null;
    }
}
        

The PersonChecker class actually extends the BaseContraint class that simplifies IConstraint implementations. It defines a default message ("{personChecker.message}") with a message key that could be used in your validation messages bundles (see above Working with Error Messages and Localization).

You should then register this new constraint in the validation framework:

ValidatorFactory.getInstance().registerConstraintClass(PersonChecker);
        

Because Flex annotations have no specific implementation, you may then directly use the constraint annotation in the Person class:

[Bindable]
[PersonChecker]
public class Person {

    [Size(min="1", max="50")]
    public var firstname;

    [Size(min="1", max="255")]
    public var lastname;
}
        

Note that the annotation isn't qualified with any package name: registering two constraint class with the same name but in different packages will result in using the last registered one only. This behavior may additionaly be used in order to override default constraint implementations: if you write your own Size constraint implementation and register it with the ValidatorFactory class, it will be used instead of the built-in one.

If the constraint exists in Java and if you use the code generation tools, the unqualified class name of the Java constraint will be generated on top of the Person class, just as above.

Tip

Don't forget the -keep-as3-metadata+=AssertFalse,...,Size,PersonChecker compiler option!

See standard constraint implementations in the GraniteDS distribution to know more about specific attributes support and other customization options.

By default, in addition to returning an array of ConstraintViolations, the validation framework will dispatch events for each failed constraint, provided that the bean that holds the property is an IEventDispatcher. These events are instances of the ConstraintViolationEvent class and are dispatched between two ValidationEvents events (start and end).

Because ActionScript3 beans annotated with the [Bindable] annotation are automatically compiled as IEventDispatcher implementations, generated beans (or other bindable beans written manually) will dispatch constraint events. You may then listen validation events dispatched by a bean if you register your event listeners as follow:

private function startValidationHandler(event:ValidationEvent):void {
    // reset all error messages...
}

private function constraintViolationHandler(event:ConstraintViolationEvent):void {
    // display the error message on the corresponding input...
}

private function endValidationHandler(event:ValidationEvent):void {
    // done...
}

...
bean.addEventListener(
    ValidationEvent.START_VALIDATION,
    startValidationHandler, false, 0, true
);
bean.addEventListener(
    ConstraintValidatedEvent.CONSTRAINT_VALIDATED,
    constraintValidatedHandler, false, 0, true
);
bean.addEventListener(
    ValidationEvent.END_VALIDATION,
    andValidationHandler, false, 0, true
);

...
ValidatorFactory.getInstance().validate(bean);
        

It may be however very tedious to add such listeners to all your beans and to write the code for displaying or reseting error messages for all inputs.

With the FormValidator component, you get an easy way to use implicitly these events: the FormValidator performs validation on the fly whenever the user enters data into user inputs and automatically displays error messages when these data are incorrect, based on constraint annotations placed on the bean properties.

A sample usage with Flex 4 (using the Person bean introduced above and bidirectional bindings):



<fx:Declarations>
    <v:FormValidator id="fValidator" form="{personForm}" entity="{person}"/>
</fx:Declarations>

<fx:Script>

    [Bindable]
    protected var person:Person = new Person();

    protected function savePerson():void {
        if (fValidator.validateEntity()) {
            // actually save the validated person entity...
        }
    }

    protected function resetPerson():void {
        person = new Person();
    }
</fx:Script>

<mx:Form id="personForm">
    <mx:FormItem label="Firstname">
        <s:TextInput id="iFirstname" text="@{person.firstname}"/>
    </mx:FormItem>
    <mx:FormItem label="Lastname" required="true">
        <s:TextInput id="iLastname" text="@{person.lastname}"/>
    </mx:FormItem>
</mx:Form>
  
<s:Button label="Save" click="savePerson()"/>
<s:Button label="Cancel" click="resetPerson()"/>
        

In the above sample, the personForm form uses two bidirectional bindings between the text inputs and the person bean. Each time the user enter some text in an input, the value of the input is copied into the bean and triggers a validation. Error messages are then automatically displayed or cleared depending on the validation result.

Note that the binding with the target entity should be direct (e.g. not entity="{model.entity}" but entity="{entity}". If not possible or too complex, you can specify a property entityPath to indicate the validator that it should bind to a deeper element in the object graph.



<fx:Declarations>
    <v:FormValidator id="fValidator" form="{personForm}" entity="{model.person}" entityPath="model"/>
</fx:Declarations>

        

The global validation of the person bean will be performed when the user click on the "Save" button. However, class-level constraint violations (such as the PersonChecker constraint) cannot be automatically associated to an input, and these violations prevent the fValidator.validateEntity() call to succeed while nothing cannot be automatically displayed to the user.

To solve this problem, three options are available:

(1) Unhandled Violations with the "properties" Argument:

[Bindable]
[PersonChecker(properties="firstname,lastname"]
public class Person {
    ...
}
        

This tell the FormValidator to display the PersonChecker error message on both firstname and lastname inputs. You may of course use only the firstname property or add another property at your convenience.

(2) Unhandled Violations with the unhandledViolationsMessage Property:



<mx:Form id="personForm">
    <mx:FormItem label="Firstname">
        <s:TextInput id="iFirstname" text="@{person.firstname}"/>
    </mx:FormItem>
    <mx:FormItem label="Lastname" required="true">
        <s:TextInput id="iLastname" text="@{person.lastname}"/>
    </mx:FormItem>
    <s:Label text="{fValidator.unhandledViolationsMessage}"/>
</mx:Form>
        

All violation messages that cannot be associated with any input will be diplayed in the label at the bottom of the form (separated by new lines).

(3) Unhandled Violations with the unhandledViolations Event:



<fx:Declarations>
    <v:FormValidator id="fValidator" form="{personForm}" entity="{person}"
        unhandledViolations="showUnhandledViolations(event)"/>
</fx:Declarations>

<fx:Script>

    protected function showUnhandledViolations(event:ValidationResultEvent ):void {
        // display unhandled messages...
    }

</fx:Script>
        

The third option let you do whatever you want with these unhandled violations. You can display the event.message somewhere (it has the same format as the unhandledViolationsMessage property), you may loop over the event.results (array of ValidationResult's) or you may even call the fValidator.getUnhandledViolations method that will give you the last unhandled ConstraintViolation instances.

With Flex 3, because bidirectional bindings are not natively supported, you would have to use mx:Binding for each input. With the above sample, you will add:



<mx:TextInput id="iFirstname" text="{person.firstname}"/>
...
<mx:TextInput id="iLastname" text="{person.lastname}"/>
...
<mx:Binding destination="person.firstname" source="iFirstname.text"/>
<mx:Binding destination="person.lastname" source="iLastname.text"/>
        

Note also that with Tide, to simplify the cancel operations, you may reset the entity state with Managed.resetEntity(entity) (see Data Management. This may be particularly useful if you are not creating a new person but modifying an existing one.

If you don't want or if you can't use bidirectional bindings, you may still use the FormValidator component but will need to specify the property validationSubField for each input:



<fx:Declarations>
    <v:FormValidator id="fValidator" form="{personForm}" entity="{person}"/>
</fx:Declarations>

<fx:Script>

    [Bindable]
    protected var person:Person = new Person();

    protected function savePerson():void {

        person.firstname = iFirstname.text == "" ? null : iFirstname.text;
        person.lastname = iLastname.text == "" ? null : iLastname.text;

        if (fValidator.validateEntity()) {
            // actually save the validated person entity...
        }
    }

    protected function resetPerson():void {
        person = new Person();
    }
</fx:Script>

<mx:Form id="personForm">
    <mx:FormItem label="Firstname">
        <s:TextInput id="iFirstname" text="{person.firstname}"
            validationSubField="firstname"/>
    </mx:FormItem>
    <mx:FormItem label="Lastname" required="true">
        <s:TextInput id="iLastname" text="{person.lastname}"
            validationSubField="lastname"/>
    </mx:FormItem>
</mx:Form>
        

This time, you have to set manually input values into your bean, but this will work with Flex 3 as well and these subfields may contain a path to a subproperty: for example, if you have an Address bean in your Person bean, you could write validationSubField="address.address1".

A last option to help the FormValidator detect the data bindings is to define a global list of properties which will be considered as UI component targets for bindings. By default, text, selected, selectedDate, selectedItem and selectedIndex are prioritarily considered for binding detection so most standard controls work correctly (for example TextInput, TextArea, CheckBox or DatePicker).

All standard constraints should behave exactly in the same way as they behave in Java, except for some advanced Pattern usages: because the regular expression support in ActionScript 3 may differ from the Java one (especially with supported ), you should be aware of few possible inconstancies between Pattern constraints written in Java and in ActionScript3.