转换和检验的主要用途是,在更新模型数据之前,确保值符合要求。这样,在调用应用程序方法来处理数据时,就可以对模型的状态做某些假设。通过使用转换和检验,可以集中精力考虑业务逻辑,而不必为输入数据的限制条件(比如空值检测、长度限制、范围边界等等)操心。
所以,应该在更新模型数据阶段中将组件数据绑定到托管 bean 模型之前执行转换和检验。正如在 “JSF 应用程序的生命周期” 一节中看到的,在处理检验阶段进行转换和检验 — 先转换,再检验。
在 JSF 中有四种检验形式:
- 内置的检验组件
- 应用程序级检验
- 后端 bean 中的检验方法(内联)
- 定制的检验组件(它们实现
Validator 接口)
本节解释这些检验形式并演示它们的使用方法。
标准检验
JSF 提供三个标准检验组件:
DoubleRangeValidator:组件的本地值必须是数字类型的;必须处于最小值、最大值或这两者指定的范围内。
LongRangeValidator:组件的本地值必须是数字类型的,并可以转换为 long;必须处于最小值、最大值或这两者指定的范围内。
LengthValidator:类型必须是 string;长度必须处于最小值、最大值或这两者指定的范围内。
在这个示例应用程序中,联系人的年龄可以是任何有效的整数。因为 -2 这样的年龄是没有意义的,所以需要给这个字段添加某些检验。清单 29 使用 <f:validateLongRange> 进行简单的检验,确保年龄字段中的数据是有意义的:
清单 29. 使用 <f:validateLongRange> 检验年龄的值是否合理
<%-- age --%> <h:outputLabel value="Age" for="age" accesskey="age" /> <h:inputText id="age" size="3" value="#{contactController.contact.age}"> <f:validateLongRange minimum="0" maximum="150"/> </h:inputText> <h:message for="age" errorClass="errorClass" />
|
在检验年龄字段之后,可能希望为名字字段指定长度限制,见清单 30。
清单 30. 确保 firstName 不是太长也不是太短
<%-- First Name --%> <h:outputLabel value="First Name" for="firstName" accesskey="f" /> <h:inputText id="firstName" label="First Name" required="true" value="#{contactController.contact.firstName}" size="10" > <f:validateLength minimum="2" maximum="25" /> </h:inputText> <h:message for="firstName" errorClass="errorClass" />
|
尽管 JSF 内置的检验在许多场景中都是有效的,但是它们的功能有限。在处理电子邮件、电话号码、URL、日期等数据时,编写自己的检验器可能更好(本节后面会讨论定 制的检验器)。还可以使用 Tomahawk、Shale、JSF-Validations 和 Crank 提供的检验器(参见 参考资料)。
应用程序级检验
从 概念上说,应用程序级检验实际上是业务逻辑检验。JSF 将表单级或字段级检验与业务逻辑检验分隔开。应用程序级检验需要在使用模型的托管 bean 方法中添加代码,以确保绑定到模型的数据的质量。例如,对于购物车来说,可以使用表单级检验确保输入的数量是有效的,但是还需要通过业务逻辑检验检查用户 是否超过了他的信用限额。这是 JSF 中关注点隔离的另一个例子。
假设用户单击一个绑定到动作方法的按钮,这个动作方法在调用应用程序阶段被调用(细节参见前面的 图 5)。在对模型数据进行任何操作之前(通常在更新模型阶段更新模型数据),可以根据应用程序的业务逻辑检查输入的数据是否是有效的。
例如,在这个示例应用程序中,用户单击 Update/Add 按钮,这个按钮绑定到应用程序控制器的 persist() 方法。可以在 persist() 方法中添加检验代码,检查系统中是否已经存在当前的 firstName/lastName 组合。如果这个联系人已经存在,那么可以在 FacesContext 中添加一个消息,然后返回 null(如果在这个动作上应用了导航规则),从而让 JSF 留在当前视图上。
我们再看一下联系人应用程序,这一次在 persist() 动作方法中执行一些应用程序级逻辑,见清单 31 和清单 32。清单 31 给出控制器中的应用程序级检验逻辑:
清单 31. 控制器中的应用程序级检验逻辑
public class ContactController { public String persist() { /* Perform the application level validation. */ try { contact.validate(); } catch (ContactValidationException contactValidationException) { addErrorMessage(contactValidationException.getLocalizedMessage()); return null; } /* Turn form off, turn link on. */ form.setRendered(false); addNewCommand.setRendered(true); /* Add a status message. */ if (contactRepository.persist(contact) == null) { addStatusMessage("Added " + contact); } else { addStatusMessage("Updated " + contact); } return "contactPersisted"; } private void addErrorMessage(String message) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage( FacesMessage.SEVERITY_ERROR, message, null)); }
|
在清单 31 中,persist() 方法调用 contact 对象上的 validate() 方法。它捕获任何异常并把异常错误消息转换为 FacesMessage。如果发生异常,它会返回 null,其含义为:留在当前视图上,不要导航到下一个视图。
实际的检验代码包含在模型中 — 即,Contact 类的 validate() 方法,见清单 32。这一点很重要:在为联系人添加更多的检验代码时,不需要修改控制器或视图层。
清单 32. 检验代码在模型中,而不在控制器中
... public class Contact implements Serializable { ... public void validate() throws ContactValidationException { if ( (homePhoneNumber == null || "".equals(homePhoneNumber)) && (workPhoneNumber == null || "".equals(workPhoneNumber)) && (mobilePhoneNumber == null || "".equals(mobilePhoneNumber)) ) { throw new ContactValidationException("At least one phone number" + "must be set"); } }
|
应用程序级检验很简单,也很容易使用。它的优点是:
- 容易实现
- 不需要单独的类(定制检验器)
- 页面作者不需要指定检验器
应用程序级检验的缺点是,它在其他形式的检验(标准、定制和组件)之后执行,而且错误消息只在执行其他形式的检验之后显示。
最后,应用程序级检验应该只用于需要业务逻辑检验的场合。
后端 bean 中的定制检验器
对 于标准 JSF 检验器不支持的数据类型(包括电子邮件地址和 ZIP 编码),需要构建自己的检验组件。如果希望对显示给最终用户的检验消息进行显式地控制,也需要构建自己的检验器。通过使用 JSF,可以创建可插入的检验组件,可以在整个 Web 应用程序中重用这些组件。
如果不想创建单独的检验器类,也可以在后端 bean 方法中实现定制的检验。这种方式对于应用程序开发人员更合适。例如,可以在托管 bean 中编写一个方法来检验电话号码,见清单 33:
清单 33. 电话号码检验
public class ContactValidators { private static Pattern phoneMask;
static { String countryCode = "^[0-9]{1,2}"; String areaCode = "(|-|\\(){1,2}[0-9]{3}(|-|\\)){1,2}"; String prefix = "(|-)?[0-9]{3}"; String number = "(|-)[0-9]{4}___FCKpd___4quot;; phoneMask = Pattern.compile(countryCode + areaCode + prefix + number); }
public void validatePhone(FacesContext context, UIComponent component, Object value) throws ValidatorException {
String sValue = (String)value;
Matcher matcher = phoneMask.matcher(sValue);
if (!matcher.matches()) { FacesMessage message = new FacesMessage(); message.setDetail("Phone number not valid"); message.setSummary("Phone number not valid"); message.setSeverity(FacesMessage.SEVERITY_ERROR); throw new ValidatorException(message); }
} ... //ADD MORE VALIDATION METHODS FOR THE APP HERE!
}
|
ContactValidators 类有一个 validatePhone() 方法。validatePhone() 方法使用 Java regex API 确保输入的字符串是有效的电话号码。如果值与模式不匹配,那么 validatePhone() 方法会抛出一个 ValidatorException。
要使用 ContactValidators 类,需要在 faces-config.xml 文件中注册它,见清单 34:
清单 34. 将 ContactValidators 注册为托管 bean
<managed-bean> <managed-bean-name>contactValidators</managed-bean-name> <managed-bean-class>com.arcmind.contact.validators.ContactValidators</managed-bean-class> <managed-bean-scope>application</managed-bean-scope> </managed-bean>
|
要使用检验器,需要对工作电话号码、家庭电话号码和移动电话号码使用 validator 属性,见清单 35:
清单 35. 通过 validator 属性在视图中使用检验器
<%-- Work --%> <h:outputLabel value="Work" for="work" accesskey="w" /> <h:inputText id="work" value="#{contactController.contact.workPhoneNumber}" size="11" validator="#{contactValidators.validatePhone}" /> <h:message for="work" errorClass="errorClass" /> <%-- Home --%> <h:outputLabel value="Home" for="home" accesskey="h" /> <h:inputText id="home" value="#{contactController.contact.homePhoneNumber}" size="11" validator="#{contactValidators.validatePhone}" /> <h:message for="home" errorClass="errorClass" /> <%-- Mobile --%> <h:outputLabel value="Mobile" for="mobile" accesskey="m" /> <h:inputText id="mobile" value="#{contactController.contact.mobilePhoneNumber}" size="11" validator="#{contactValidators.validatePhone}" /> <h:message for="mobile" errorClass="errorClass" />
|
可以看到,这里把 validatePhone() 方法绑定到 <h:inputText> 组件:<h:inputText id="mobile" ... validator="#{contactValidators.validatePhone}"。
对于应用程序开发人员来说,使用托管 bean 执行检验是不错的方法。但是,如果要开发可重用的框架或可重用的组件集,那么最好创建单独的定制检验器。
单独的定制检验器
可以使用 JSF 创建可插入的检验组件,这些组件可以在整个 Web 应用程序中重用。
要创建定制的检验器,需要执行以下步骤:
- 创建一个实现
Validator 接口(javax.faces.validator.Validator)的类。
- 实现
validate() 方法。
- 在 faces-config.xml 文件中注册定制的检验器。
- 在 JSP 中使用
<f:validator/> 标记。
我们来逐一介绍这些步骤,并提供创建定制检验器的示例代码。
步骤 1:实现 Validator 接口
第一步是实现 Validator 接口,见清单 36:
清单 36. 实现 Validator 接口
package com.arcmind.validators;
import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException;
import java.util.regex.Matcher; import java.util.regex.Pattern; public class ZipCodeValidator implements Validator {
/** Accepts zip codes like 85710 */ private static final String ZIP_REGEX = "[0-9]{5}";
/** Optionally accepts a plus 4 */ private static final String PLUS4_OPTIONAL_REGEX = "([ |-]{1}[0-9]{4})?";
private static Pattern mask = null;
static { mask = Pattern.compile(ZIP_REGEX + PLUS4_OPTIONAL_REGEX); }
|
步骤 2:实现 validate() 方法
接下来,需要实现 validate() 方法,见清单 37:
清单 37. 实现 validate() 方法
public class ZipCodeValidator implements Validator {
... public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
/* Get the string value of the current field */ String zipField = (String) value;
/* Check to see if the value is a zip code */ Matcher matcher = mask.matcher(zipField);
if (!matcher.matches()) {
FacesMessage message = new FacesMessage(); message.setDetail("Zip code not valid"); message.setSummary("Zip code not valid"); message.setSeverity(FacesMessage.SEVERITY_ERROR); throw new ValidatorException(message); }
} }
|
步骤 3:注册定制检验器
现在,您应该对向 FacesContext 注册定制检验器的代码很熟悉了(见清单 38):
清单 38. 在 faces-config.xml 中注册定制检验器
<validator> <validator-id>arcmind.zipCode</validator-id> <validator-class>com.arcmind.validators.ZipCodeValidator</validator-class> </validator>
|
步骤 4:在 JSP 中使用 <f:validator> 标记
<f:validator/> 标记声明使用 zipCode 检验器,见清单 39:
清单 39. 在 JSP 中使用 <f:validator> 标记
<%-- zip --%> <h:outputLabel value="Zip" for="zip" accesskey="zip" /> <h:inputText id="zip" size="5" value="#{contactController.contact.zip}"> <f:validator validatorId="arcmind.zipCode"/> </h:inputText> <h:message for="zip" errorClass="errorClass" />
|
总之,创建定制检验器是非常容易 的,而且这些检验器可以跨许多应用程序重用。缺点是必须创建一个单独的类,并在 faces 上下文中管理检验器的注册。但是,可以进一步改进定制检验器的实现:创建一个使用这个检验器的定制标记,使它看起来像内置的检验。对于需要经常检验的数 据,比如电子邮件地址,这种方法可以简化代码,尽可能增加代码重用和提高应用程序行为的一致性。
再论检验和转换
在到达检验阶段之前,转换已经执行过了。例如,如果有一个 int 属性绑定到 inputText 字段,那么先对这个字段进行转换,然后再进行检验。
假设您有一个 PhoneNumber 值对象,并使用它(而不是使用 String)在 Contact 中存储电话号码。那么 清单 33 中电话号码的检验规则就没什么意义了。实际上,这个检验规则只证明 String 采用了电话号码的格式。这个逻辑实际上应该放在转换器中,见清单 40:
清单 40. 再论检验和转换:PhoneConverter
package com.arcmind.contact.converter;
import java.util.regex.Pattern;
import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; import javax.faces.convert.ConverterException;
import com.arcmind.contact.model.PhoneNumber;
/** * @author Richard Hightower * */ public class PhoneConverter implements Converter { private static Pattern phoneMask; static { String countryCode = "^[0-9]{1,2}"; String areaCode = "( |-|\\(){1,2}[0-9]{3}( |-|\\)){1,2}"; String prefix = "( |-)?[0-9]{3}"; String number = "( |-)[0-9]{4}___FCKpd___11quot;; phoneMask = Pattern.compile(countryCode + areaCode + prefix + number); }
public Object getAsObject(FacesContext context, UIComponent component, String value) { System.out.println("PhoneConverter.getAsObject()");
if (value.isEmpty()) { return null; } /* Before we parse, let's see if it really is a phone number. */ if (!phoneMask.matcher(value).matches()) { FacesMessage message = new FacesMessage(); message.setDetail("Phone number not valid"); message.setSummary("Phone number not valid"); message.setSeverity(FacesMessage.SEVERITY_ERROR); throw new ConverterException(message); } /* Now let's parse the string and populate a phone number object. */ PhoneNumber phone = new PhoneNumber(); phone.setOriginal(value); String[] phoneComps = value.split("[ ,()-]"); String countryCode = phoneComps[0]; phone.setCountryCode(countryCode);
if ("1".equals(countryCode) && phoneComps.length == 4) { phone.setAreaCode(phoneComps[1]); phone.setPrefix(phoneComps[2]); phone.setNumber(phoneComps[3]); } else if ("1".equals(countryCode) && phoneComps.length != 4) { throw new ConverterException(new FacesMessage( "No Soup for you butter fingers!")); } else if (phoneComps.length == 1 && value.length() > 10){ phone.setCountryCode(value.substring(0,1)); phone.setAreaCode(value.substring(1,4)); phone.setPrefix(value.substring(4,7)); phone.setNumber(value.substring(7)); } else { phone.setNumber(value); } return phone; }
public String getAsString(FacesContext context, UIComponent component, Object value) { System.out.println("PhoneConverter.getAsString()"); return value.toString(); } }
|
与检验器不同,转换器的好处是可以在 faces-config.xml 中注册(见 清单 27),让转换器连接到某个类。每当这个类出现在表达式语言(EL)值绑定中时,会自动使用这个转换器;不需要在 JSP 中添加 <f:converter>。新的电话号码转换器会自动应用于 PhoneNumber,不需要在视图中指定转换器。
原来的电话号码检验成了电话号码转换的一部分,您可能想知道电话号码检验器现在是什么样子。可以通过编写一个检验器来回答这个问题,它证明电话号码属于亚利桑那州,见清单 41:
清单 41. 确保电话号码属于亚利桑那州
package com.arcmind.contact.validators;
import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.validator.ValidatorException;
import com.arcmind.contact.model.PhoneNumber;
public class ContactValidators {
public void validatePhone(FacesContext context, UIComponent component, Object value) throws ValidatorException {
System.out.println("ContactValidators.validatePhone()"); PhoneNumber phoneNumber = (PhoneNumber)value;
if (!phoneNumber.getAreaCode().equals("520") && !phoneNumber.getAreaCode().equals("602")) { FacesMessage message = new FacesMessage(); message.setDetail("Arizona residents only"); message.setSummary("Arizona residents only"); message.setSeverity(FacesMessage.SEVERITY_ERROR); throw new ValidatorException(message); }
}
}
|
注意,与前面的检验器不同,这个电话号码检验器并不处理 String。在调用它之前,已经调用了转换器。因此,值并不是 String,而是 PhoneNumber。