Validator
User Input Validation
User input validation is an indispensable function of a web application. ZK's validator can help developers to accomplish this task. The validator is a reusable element that performs validation and stores validation messages into a validation message holder. When it's applied, it's invoked before saving data to binding target (ViewModel or middle object). When you bind a component's attribute to a validator, binder will use it to validate attribute's value automatically before saving to a ViewModel or to a middle object. If validation fails, ViewModel's (or middle object's) properties will be unchanged.
We usually provide custom validator with ViewModel's property.
public void class MyViewModel{
private Validator emailValidator = new EmailValidator();
public Validator getEmailValidator(){
return emailValidator;
}
}
Then it can be referenced on a ZUL.
<textbox value="@save(vm.account.email) @validator(vm.emailValidator)"/>
<textbox value="@save(vm.account.email) @validator('foo.EmailValidator')"/>
- Binder use vm.emailValidtor to validate textbox's value before saving to vm.account.email. If validation fails, ViewModel's property: vm.account.email will keep unchanged.
<textbox value="@save(vm.account.email, before='save') @validator(vm.emailValidator)"/>
- Binder use vm.emailValidtor to validate textbox's value before executing Command 'save'.
Following is a comparison table comparing above two saving syntax:
@bind(vm.account.email) | @load(vm.selected.price) @save(vm.account.email, before= 'save') | |
a component's attribute related event fires (e.g. onChange for value) | Before executing a command. | |
Not save data to ViewModel | Not save data to ViewModel & Not execute commands | |
Immediately validate for single field | Batch save & validate all fields. | |
|
|
In form binding, you can apply a validator on "form" attribute or an input component. If you apply on both places, you can double validate user input. The first time is when saving data to middle object, this can give a user immediate response after input. The second time is when saving to a ViewModel upon a command, this can validate user input even he doesn't input anything and submit empty data directly.
<groupbox form="@id('fx') @load(vm.selected) @save(vm.selected, before='saveOrder') @validator(vm.formValidator)">
<grid hflex="true" >
<columns>
<column width="120px"/>
<column/>
</columns>
<rows>
<row>Id
<hlayout>
<label value="@load(fx.id)"/>
</hlayout> </row>
<row>Description <textbox value="@bind(fx.description)"/></row>
<row>Quantity
<intbox value="@bind(fx.quantity) @validator(vm.quantityValidator)"/>
</row>
<row>Price
<doublebox value="@bind(fx.price) @validator(vm.priceValidator)" format="###,##0.00" />
</row>
</rows>
</grid>
</groupbox>
- You can apply validators on form attribute and each input component respectively. (line 1,14)
- ZK will invoke "vm.formValidator" before executing command 'saveOrder. (line 1)
- ZK will validate "quantity" when onChange event fires on intbox (when a user blur the focus). (line 14)
Validation Message Holder
ZK provides a standard mechanism to store and display validation message. After performing validation, the validator might store a validation message in validation message holder. To use it, you have to initialize it by specifying its id in validationMessages attribute with the @id . Then you can reference it with this id.
<window apply="org.zkoss.bind.BindComposer" viewModel="@id('vm')@init('foo.MyViewModel')"
validationMessages="@id('vmsgs')">
</window>
Validation message holder stores messages like a map with key-value pairs. The value is the validation messages that generated by validators after the validation is performed, and the default key is the binding source component (object itself, no id) that bound to validators. To retrieve and display validation message in a ZUL, you can bind a display component, i.e. label, to validation message holders with a component as the key.
The binder will reload validation messages each time after validation.
Display validation message with default key
<window apply="org.zkoss.bind.BindComposer" viewModel="@id('vm')@init('foo.MyViewModel')"
validationMessages="@id('vmsgs')">
<hlayout>Value1:
<textbox id="tb1" value="@bind(vm.value1) @validator(vm.validator1)" />
<label id="m1" value="@bind(vmsgs[tb1])"/>
</hlayout>
<hlayout>Value2:
<intbox id="tb2" value="@bind(vm.value2) @validator(vm.validator2)" />
<label id="m2" value="@bind(vmsgs[self.previousSibling])"/>
</hlayout>
</window>
- You can use component's id to reference a component object. (line 5)
- You can use component's property to reference a component object with relative position that eliminates giving component an id . (line 9)
Implement a Validator
You can create custom validators upon your application's requirement by implementing Validator interface or inheriting AbstractValidator for convenience.
Property Validator
Single Property Validation
We usually need to validate one property at one time, the simplest way is to inherit AbstractValidator and override validate() to implement a custom validation rule. If validation fails, use addInvalidMessage() to store validation messages to be displayed. You have to pass validator bound component object as a key to retrieve this message like we mention in section Validation Message Holder.
<intbox value="@save(vm.quantity) @validator(vm.rangeValidator)"/>
public Validator getRangeValidator(){
return new AbstractValidator() {
public void validate(ValidationContext ctx) {
Integer val = (Integer)ctx.getProperty().getValue();
if(val<10 || val>100){
addInvalidMessage(ctx, "value must not < 10 or > 100, but is "+val);
}
}
};
}
- We can get the user input data from validator2's binding source component by ctx.getProperty().getValue() . (line 4)
- addInvalidMessage() will add message into validation message holder. (line 6)
Dependent Property Validation
We sometimes need another property's value to validate the current property. We have to save those values that have dependency among them upon the same Command (use @save(vm.p, before='command' ) , thus binder will pass those properties that are saved upon the same command to ValidationContext and we can retrieve them with property's key to process.
Assume shipping date must be at least 3 days later than creation date.
<window apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('eg.ValidationMessagesVM')"
validationMessages = "@id('vmsgs')">
<grid hflex="true" >
<columns>
<column width="120px"/>
<column/>
</columns>
<rows>
<!-- other input fields -->
<row>Creation Date
<hlayout>
<datebox id="cdBox"
value="@load(vm.selected.creationDate) @save(vm.selected.creationDate, before='saveOrder')
@validator(vm.creationDateValidator)"/>
<label value="@load(vmsgs[cdBox])" sclass="red" />
</hlayout>
</row>
<row>Shipping Date
<hlayout>
<datebox id="sdBox"
value="@load(vm.selected.shippingDate) @save(vm.selected.shippingDate, before='saveOrder')
@validator(vm.shippingDateValidator)"/>
<label value="@load(vmsgs[sdBox])" sclass="red" />
</hlayout>
</row>
</rows>
</grid>
</window>
- As we save vm.selected.creationDate and vm.selected.shippingDate before Command 'saveOrder', they'll be passed into validation context.
Our custom shipping date validator should get the creation date to compare.
public class ShippingDateValidator extends AbstractValidator{
public void validate(ValidationContext ctx) {
Date shipping = (Date)ctx.getProperty().getValue();//the main property
Date creation = (Date)ctx.getProperties("creationDate")[0].getValue();//the collected
//multiple fields dependent validation, shipping date have to large than creation more than 3 days.
if(!isDayAfter(creation,shipping,3)){
addInvalidMessage(ctx, "must large than creation date at least 3 days");
}
}
static public boolean isDayAfter(Date date, Date laterDay , int day) {
if(date==null) return false;
if(laterDay==null) return false;
Calendar cal = Calendar.getInstance();
Calendar lc = Calendar.getInstance();
cal.setTime(date);
lc.setTime(laterDay);
int cy = cal.get(Calendar.YEAR);
int ly = lc.get(Calendar.YEAR);
int cd = cal.get(Calendar.DAY_OF_YEAR);
int ld = lc.get(Calendar.DAY_OF_YEAR);
return (ly*365+ld)-(cy*365+cd) >= day;
}
}
- Shipping date is the main property to be validated. The way to retrieve other properties is different the main one. (line 5)
Dependent Property Validator in Form Binding
If you want to validate a property according to another property's value in the form binding, you have to apply a validator in form attribute instead of an input component that bound to a middle object's property. Then you can get all properties from validation context.
The following is the example to demonstrate the same shipping date validator but it's used in form binding.
Using validator in form binding
<groupbox form="@id('fx') @load(vm.selected)
@save(vm.selected, before='@validator(vm.shippingDateValidator)">
<grid hflex="true" >
<columns>
<column width="120px"/>
<column/>
</columns>
<!-- other components -->
<rows>
<row>Creation Date
<hlayout>
<datebox id="cdBox" value="@bind(fx.creationDate) @validator(vm.creationDateValidator)"/>
<label value="@load(vmsgs[cdBox])" sclass="red" />
</hlayout>
</row>
<row>Shipping Date
<hlayout>
<datebox id="sdBox" value="@bind(fx.shippingDate)"/>
<label value="@load(vmsgs[sdBox])" sclass="red" />
</hlayout>
</row>
</rows>
</grid>
</groupbox>
- Apply a validator in form binding, not in individual input component that bound to middle object's property. (line 1)
Validator for form binding
public Validator getShippingDateValidator() {
return new AbstractValidator(){
public void validate(ValidationContext ctx) {
Date shipping = (Date)ctx.getProperties("shippingDate")[0].getValue();
Date creation = (Date)ctx.getProperties("creationDate")[0].getValue();
//dependent validation, shipping date have to later than creation date for more than 3 days.
if(!CaldnearUtil.isDayAfter(creation,shipping,3)){
addInvalidMessage(ctx,"must large than creation date at least 3 days");
}
}
};
}
- The value main property is a Form when applying to a form binding. We should retrieve all properties with its key. (line 4-5)
- Since 6.0.1, you could get values of the bean by ValidationContext.getProperties(Object)
public Validator getShippingDateValidator() {
return new AbstractValidator(){
public void validate(ValidationContext ctx) {
//base of main property is the bean, you can get all properties of the bean
Map<String,Property> beanProps = ctx.getProperties(ctx.getProperty().getBase());
Date shipping = (Date)beanProps.get("shippingDate").getValue();
Date creation = (Date)beanProps.get("creationDate").getValue();
//....
}
};
}
== Pass and Retrieve Parameters ==
We can pass one or more parameters by EL expression to a validator. The parameters are written in key-value pairs format inside <tt> @validator() </tt> annotation.
'''Passing a constant value in a ZUL'''
<source lang="xml">
<textbox id="keywordBox" value="@save(vm.keyword) @validator(vm.maxLengthValidator, length=3)"/>
Retrieving parameters in a validator
public class MaxLengthValidator implements Validator {
public void validate(ValidationContext ctx) {
Number maxLength = (Number)ctx.getBindContext().getValidatorArg("length");
if (ctx.getProperty().getValue() instanceof String){
String value = (String)ctx.getProperty().getValue();
if (value.length() > maxLength.longValue()){
ctx.setInvalid();
}
}else{
ctx.setInvalid();
}
}
}
- The parameter's name "length" is user defined.
Passing object in a ZUL
<combobox id="upperBoundBox" >
<comboitem label="90" value="90"/>
<comboitem label="80" value="80"/>
<comboitem label="70" value="70"/>
<comboitem label="60" value="60"/>
<comboitem label="50" value="50"/>
</combobox>
<intbox value="@save(vm.quantity) @validator(vm.upperBoundValidator, upper=upperBoundBox.selectedItem.value)"/>
- Pass a value from another component's attribute.
Self-defined Validation Message Key
You might want to set the key of a validation message on your own, especially when you use single validator to validate multiple fields. As all validation messages generated by one validator for a component have the same default key (the component object itself), in order to retrieve and display a validation message individually, you have to set different key for each message.
Assume you use only one validator in a form binding to validate all fields.
public Validator getFormValidator(){
return new AbstractValidator() {
public void validate(ValidationContext ctx) {
String val = (String)ctx.getProperties("value1")[0].getValue();
if(invalidCondition01(val)){
addInvalidMessage(ctx, "fkey1", "value1 error");
}
val = (String)ctx.getProperties("value2")[0].getValue();
if(invalidCondition02(val)){
addInvalidMessage(ctx, "fkey2", "value2 error");
}
val = (String)ctx.getProperties("value3")[0].getValue();
if(invalidCondition03(val)){
addInvalidMessage(ctx, "fkey3", "value3 error");
}
}
};
}
- Because validation messages are stored in the same context, you should use different keys for different messages.
Display validation message aside each component
<vbox form="@id('fx') @load(vm) @save(vm,before='submit') @validator(vm.formValidator)">
<hbox><textbox id="t41" value="@bind(fx.value1)"/><label id="l41" value="@bind(vmsgs['fkey1'])"/></hbox>
<hbox><textbox id="t42" value="@bind(fx.value2)"/><label id="l42" value="@bind(vmsgs['fkey2'])"/></hbox>
<hbox><textbox id="t43" value="@bind(fx.value3)"/><label id="l43" value="@bind(vmsgs['fkey3'])"/></hbox>
<button id="submit" label="submit" onClick="@command('submit')" />
</vbox>
Validation in Non-Conditional Property Binding
The execution of a non-conditional property binding is separated from execution of command. If an event triggers both property saving and command execution, they don't block each other.
For example:
<textbox value="@bind(vm.value) @validator(vm.myValidator)" onChange="@command('submit')"/>
- When onChange event fires, binder will perform validation before saving vm.value. No matter validation fails or not, it will execute command "submit". Validation failure only stops binder saveing textbox's value into vm.value.
For another opposite example:
<textbox id="t1" value="@bind(vm.foo) " onChange="@command('submit')"/>
<textbox id="t2" value="@save(vm.bar, before='submit') @validator(vm.myValidator)"/>
- When onChange event fires, if t2's value fails in validation, it will stop the binder to execute command "submit". But the binder will still save t1's value to vm.foo.
Use Built-in Validator
ZK provides some built-in validators and you can use it directly without implementing on your own. It's used with syntax @validator('validatorName') and you should fill built-in validator's name as a string.
Bean Validator
This validator integrates Java Bean Validation ([1] ) framework that defines a metadata model and API [2] to validate JavaBeans. Developers can specify the constraints of a bean's property by Java annotations and validate against the bean by API. By using ZK's bean validator, you only have to specify constraints on bean's properties then bean validator will invoke API to validate for you.
Prepare to Use JSR 303
Required Jars
A known implementation of JSR 303 is Hibernate Validator. The following is a sample dependency list in pom.xml for using Hibernate Validator:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.0.2.GA</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
Hibernate Validator assumes the use of log4j to log information. You can use any log4j implementation you prefer.
Setup
Under project classpath, you need to prepare
- log4j.properties
- META-INF/validation.xml
Here are examples of minimal settings in these files:
log4j.properties
log4j.rootLogger=info, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
META-INF/validation.xml
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration">
<default-provider>org.hibernate.validator.HibernateValidator</default-provider>
<message-interpolator>org.hibernate.validator.engine.ResourceBundleMessageInterpolator</message-interpolator>
<traversable-resolver>org.hibernate.validator.engine.resolver.DefaultTraversableResolver</traversable-resolver>
<constraint-validator-factory>org.hibernate.validator.engine.ConstraintValidatorFactoryImpl</constraint-validator-factory>
</validation-config>
You can customize the setting depending on your requirement.
Usage
Add constraint in JavaBean's property with Java annotation.
public static class User{
private String _lastName = "Chen";
@NotEmpty(message = "Last name can not be null")
public String getLastName() {
return _lastName;
}
public void setLastName(String name) {
_lastName = name;
}
}
Use this validator with its name beanValidator.
<window id="win" apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init(foo.MyViewModel)"
validationMessages="@id('vmsgs')">
<textbox id="tb" value="@bind(vm.user.lastName) @validator('beanValidator')" />
<label value="@load(vmsgs[tb])"/>
</window>
Since 6.0.1, it also supports to validate a property of a form which load properties from a bean [3].
<grid form="@id('fx') @load(vm.user) @save(vm.user,after='save')">
<textbox id="tb" value="@bind(fx.lastName) @validator('beanValidator')"/>
<label value="@load(vmsgs[tb])"/>
</grid>
- ↑ http://jcp.org/en/jsr/detail?id=303 JSR 303
- ↑ http://jackson.codehaus.org/javadoc/bean-validation-api/1.0/index.html
- ↑ It uses the class of last loaded bean of the form to perform the validation, which means it doesn't support to validate a form that didn't loaded a bean yet.
Form Bean Validator
since 6.0.1
Like Bean Validator, this validator integrates JavaBean Validation too and validates a bean's all saving properties. About the configuration and JavaBean usage, please refer to #Prepare_to_Use_JSR_303
Usage
Use this validator with its name formBeanValidator and set a unique[1] prefix key by prefix argument of validator. When any property of the bean is invalid, it puts the invalid message to validation message holder with key prefix+propertyName.
<window id="win" apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init(foo.MyViewModel)"
validationMessages="@id('vmsgs')">
<grid width="600px" form="@id('fx') @load(vm.user) @save(vm.user,after='save')
@validator('formBeanValidator',prefix='p_')">
<textbox value="@bind(fx.firstName)"/>
<label value="@load(vmsgs['p_firstName'])"/>
</grid>
<!--more components-->
</window>
- ↑ The prefix have to be unique in the same binder.
References
Version History
Version | Date | Content |
---|---|---|
6.0.0 | February 2012 | The MVVM was introduced. |