三、引擎盖下的 JSF——第二部分
JSF 转换和验证是 JSF 框架中最重要的主题之一,本章将详细介绍。本章说明了转换和验证在 JSF 请求处理生命周期中的工作原理。您将学习如何在您的 JSF 应用中使用和定制标准的 JSF 转换器和验证器,以及当标准的转换器和验证器不能完全满足应用的需求时,如何创建您自己的定制转换器和验证器。最后,您将了解如何利用 Java Bean Validation(JSR 303)API 来支持和标准化您的 JSF 应用验证。
JSF 生命周期中的转换和验证
正如我们从第一章中了解到的,转换是将 HTTP 请求参数转换为相应的 Java 类型,以消除开发人员为每个 web 应用实现该功能所需的开销,而验证是针对特定条件验证用户输入。JSF 生命周期中的转换和验证过程可以分三个阶段进行,如图图 3-1 所示。
图 3-1 。可能发生转换和验证的 JSF 阶段
下图说明了 JSF 转换和验证是如何在
- 流程验证阶段,针对所有未将 immediate 属性设置为 true 的组件。
- 为 immediate=“真”的组件应用请求值阶段。
- 渲染响应阶段。
在流程验证阶段和应用请求值阶段,都会发生从 HTTP 请求字符串到 Java 类型的转换(使用 JSF 转换器接口中的 getAsObject API),然后执行 JSF 验证。在呈现响应阶段,会发生从 Java 类型到字符串的转换(使用 JSF 转换器接口中的 getAsString API ),以便为呈现做好准备。
注意注意,immediate 属性既可以应用于 UICommand 组件(如 CommandButton 和 CommandLink),也可以应用于 EditableValueHolder 组件(如 inputText)。
转换可以应用于所有 ValueHolder 组件(这包括 UIOutput 和 UIInput—它扩展了 ui output—组件),而验证只能应用于 EditableValueHolder 组件(这包括 ui input 组件)。在接下来的部分中,将详细说明 JSF 转换和验证。
转换
为了理解 JSF 转换,我们需要知道三个主题:转换器接口 API,标准 JSF 转换器,以及最后如何在 JSF 构建一个定制的转换器。接下来的小节将详细阐述这些主题。
转换器接口
所有 JSF 转换器都必须实现 javax.faces.convert.Converter 接口。Converter 接口描述了一个 Java 类,该类可以在模型数据对象和适合于呈现的这些模型对象的字符串表示之间执行对象到字符串和字符串到对象的转换。清单 3-1 显示了 JSF 转换器接口。
清单 3-1。 JSF 转换器界面
package javax.faces.convert;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
public interface Converter {
public Object getAsObject(FacesContext context, UIComponent component, String value);
public String getAsString(FacesContext context, UIComponent component, Object value);
}
如转换器接口所示,它包含两个 API:
第一个 API 是 getAsObject,,它执行字符串到对象的转换;可以在流程验证阶段(或应用请求值阶段)调用此 API。它有三个参数:
- 上下文,表示此请求的 JSF FacesContext 实例。
- component,它表示其值将被转换的组件。如果转换器需要使用组件属性,可以使用这个组件实例来检索组件属性。
- 值,它表示要转换为对象的字符串值。
第二个 API 是 getAsString,,它执行对象到字符串的转换;这个 API 在渲染响应阶段被调用。它需要三个参数:
- 上下文,表示此请求的 JSF FacesContext 实例。
- component,它表示其值将被转换的组件。如果转换器需要使用组件属性,可以使用这个组件实例来检索组件属性。
- 值,它表示要转换为字符串的对象值。
每个组件可以有一个或多个转换器。如果由于错误导致转换无法执行,转换器必须抛出 converter exception;在这种情况下,拥有转换器的组件将被标记为无效,ConverterException 消息将被接收并添加到 FacesContext 消息中,以便由< h:message >和< h:messages >组件显示。清单 3-2 展示了一个例子,说明了 JSF 转换是如何为带有附加转换器的多个值持有者组件工作的。
清单 3-2。 多个 ValueHolder 组件的转换示例
<h:form>
<h1>Test form</h1>
<h:outputText value="Enter First Number: "/>
<h:inputText id="firstNumber"
value="#{testBean.firstNumber}">
<f:convertNumber/>
</h:inputText>
<h:message for="firstNumber"/>
<br/>
<h:outputText value="Enter Second Number: "/>
<h:inputText id="secondNumber"
value="#{testBean.secondNumber}">
<f:convertNumber/>
</h:inputText>
<h:message for="secondNumber"/>
<br/>
<h:commandButton value="submit"/>
</h:form>
在这个例子中,我们有一个包含两个 inputText 组件的表单。每个 inputText 组件都有一个附属的转换器。<f:convert Number/>converter 将用户在输入文本中输入的值转换为 Java Number 对象,然后与 TestBean 托管 Bean 的属性(firstNumber 和 secondNumber)绑定。如果用户在两个 inputText 组件中输入非数字值,然后单击“提交”按钮,用户将在附加到每个输入文本的两个< h:message/ >组件中看到两个转换错误消息。这是因为 JSF 转换(以及 JSF 验证)必须应用于每个具有转换器(和/或验证器)的组件,这意味着 JSF 验证过程(包括转换和验证)不能中途中止,因为框架必须一次性收集所有的错误消息。
标准 JSF 转换器
现在,让我们深入了解 JSF 标准转换器的细节。表 3-1 显示了 JSF 标准转换器。
表 3-1 。JSF 标准转换器
|
转换器 ID
|
描述
|
| — | — |
| javax.faces.Boolean
| 可以应用于Boolean
和boolean
Java 类型的隐式转换器。 |
| javax.faces.Byte
| 可以应用于Byte
和byte
Java 类型的隐式转换器。 |
| javax.faces.Character
| 可以应用于Character
和char
Java 类型的隐式转换器。 |
| javax.faces.Short
| 可以应用于Short
和short
Java 类型的隐式转换器。 |
| javax.faces.Integer
| 可以应用于Integer
和int
Java 类型的隐式转换器。 |
| javax.faces.Long
| 可以应用于Long
和long
Java 类型的隐式转换器。 |
| javax.faces.Float
| 可以应用于Float
和float
Java 类型的隐式转换器。 |
| javax.faces.Double
| 可以应用于Double
和double
Java 类型的隐式转换器。 |
| javax.faces.BigDecimal
| 可以应用于BigDecimal
Java 类型的隐式转换器。 |
| javax.faces.BigInteger
| 可以应用于BigInteger
Java 类型的隐式转换器。 |
| javax.faces.Number
| 可用于将用户输入转换为Number
Java 类型的显式转换器。 |
| javax.faces.DateTime
| 可用于将用户输入转换为java.util.Date
Java 类型的显式转换器。 |
如表所示,JSF 转换器有两种类型:
- 隐式转换器。
- 显式转换器。
隐式转换器自动应用于列出的值类型。例如,假设我们有下面的计算器管理 bean,如清单 3-3 所示。
清单 3-3。 计算器托管豆
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class Calculator implements Serializable {
private Double firstNumber;
private Double secondNumber;
private Double result;
public Double getFirstNumber() {
return firstNumber;
}
public void setFirstNumber(Double firstNumber) {
this.firstNumber = firstNumber;
}
public Double getSecondNumber() {
return secondNumber;
}
public void setSecondNumber(Double secondNumber) {
this.secondNumber = secondNumber;
}
public Double getResult() {
return result;
}
public void setResult(Double result) {
this.result = result;
}
public String calculateSum() {
result = firstNumber + secondNumber;
return null;
}
}
清单 3-4 显示了使用计算器管理 bean 的 XHTML 文件。
清单 3-4。 计算器 XHTML 关联页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Calculator</title>
</h:head>
<h:body>
<h:form>
<h:panelGrid columns="2">
<h:outputText value="First number:"/>
<h:inputText id="fNumber"
value="#{calculator.firstNumber}"
required="true">
</h:inputText>
<h:outputText value="Second number:"/>
<h:inputText id="sNumber"
value="#{calculator.secondNumber}"
required="true">
</h:inputText>
</h:panelGrid>
<h:commandButton action="#{calculator.calculateSum}" value="Sum"/><br/>
<h:outputText value="Result: #{calculator.result}"/>
</h:form>
</h:body>
</html>
使用隐式 javax.faces.Double 转换器将#{calculator.firstNumber}和#{calculator.secondNumber}表达式转换为 Double 后,这两个表达式会自动绑定到 calculator 托管 bean 的 firstNumber 和 secondNumber 属性。与 Double 类型一样,隐式转换器也应用于 Boolean、Byte、Character、Short、Integer、Long、Float、BigDecimal 和 BigInteger Java 类型。
显式转换器必须显式地附加到组件上。目前,JSF 核心标签库提供了以下代表 JSF 显式转换器的标签:
converter 将输入字符串转换成任意指定格式的 java.util.Date 对象。清单 3-5 展示了一个包含 java.util.Date 属性的样例受管 bean。
清单 3-5。 包含日期属性的托管 Bean 示例
import java.io.Serializable;
import java.util.Date;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class TestBean implements Serializable {
// ...
private Date birthDate;
public Date getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
// ...
}
为了将输入字符串转换为“dd-MM-yyyy”格式的生日属性,您可以将附加到 EditableValueHolder 组件,如下所示:
<h:outputText value="Date of birth:"/>
<h:inputText id="birthDate"
value="#{testBean.birthDate}"
required="true">
<f:convertDateTime pattern="dd-MM-yyyy"/>
</h:inputText>
如果用户在“出生日期”字段中输入一个不能转换为“dd-MM-yyyy”格式的日期对象的值,将显示一条转换错误消息。清单 3-2 展示了一个< f:convertNumber >转换器的例子,它将输入字符串转换成一个 Java 数字对象。此外,它还有许多格式化功能。假设我们有 TestBean 管理的 bean #{testBean.someNumber}的 some number(Double 类型)属性,并且我们希望将其格式化为数字格式###,###。###;这可以通过使用< f:convertNumber >的模式属性来实现,如下所示:
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber pattern="###,###.###"/>
</h:outputText>
假设#{testBean.someNumber}的计算结果为 123456.124(例如),它将显示为 123,456.124。
注你可以在
docs . Oracle . com/javase/7/docs/API/Java/text/decimal format . html
了解更多关于 Java NumberFormat 的信息
如果我们只想显示#{testBean.someNumber}数字的两位小数,我们可以使用标签的 maxFractionDigits 属性,如下所示:
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber maxFractionDigits="2"/>
</h:outputText>
例如,如果#{testBean.someNumber}的计算结果为 123456.124,它将显示为 123,456.12。
使用的 currencyCode 和 type (="currency ")属性,我们可以将数字格式化为货币格式,如下所示:
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber currencyCode="EGP" type="currency"/>
</h:outputText>
例如,如果#{testBean.someNumber}的计算结果为 2000,它将显示为 EGP2,000.00。
注货币代码在 ISO 4217 定义。你可以从 http://en.wikipedia.org/wiki/ISO_4217 得到货币代码的完整列表
使用的 type="percent "属性,您可以将数字格式化为百分比。
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber type="percent"/>
</h:outputText>
例如,如果#{testBean.someNumber}的评估值为 0.3,它将显示为 30%。
注意注意,您可以通过使用 EditableValueHolders 的 converterMessage 属性来覆盖不同的转换消息。例如:<h:input text id = " some number " value = " # { bean . some number } " converter message = “不是数字!!!”/ >
如果数字转换失败,将显示转换错误消息“不是数字”。
构建定制 JSF 转换器
除了 JSF 框架提供的所有提到的隐式和显式转换器,JSF 允许开发人员创建他们自己的自定义转换器。让我们看一个例子来说明这个想法。假设我们需要将用户输入字符串转换为位置对象。为了达到这个要求,我们需要开发一个定制的转换器(例如 LocationConverter ),它将输入字符串转换为位置对象,然后将位置对象转换为友好的字符串,该字符串可以在呈现响应阶段显示给最终用户。清单 3-6 显示了我们需要用户输入转换到的位置类。
清单 3-6。 位置类
package com.jsfprohtml5.example.model;
public class Location {
private String address;
private String city;
private String country;
public Location() {
}
public Location(String address, String city, String country) {
this.address = address;
this.city = city;
this.country = country;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
Location 类是一个简单的 Java bean,包含三个属性(地址、城市、国家)。为了实现我们的 LocationConverter 自定义转换器,我们需要扩展 JSF 转换器接口并实现 getAsObject 和 getAsString 方法。清单 3-7 显示了 LocationConverter 的实现。
清单 3-7。 LocationConverter 类
package com.jsfprohtml5.example.converters;
import com.jsfprohtml5.example.model.Location;
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 javax.faces.convert.FacesConverter;
@FacesConverter("com.jsfprohtml5.LocationConverter")
public class LocationConverter implements Converter {
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (null == value || 0 == value.length()) {
return null;
}
String locationParts[] = value.split(",");
if (locationParts.length != 3
|| locationParts[0].length() == 0
|| locationParts[1].length() == 0
|| locationParts[2].length() == 0) {
FacesMessage message = new FacesMessage("Invalid Location format (address, city, country).",
"Use the following format {address, city, country)}.");
message.setSeverity(FacesMessage.SEVERITY_ERROR);
throw new ConverterException(message);
}
String address = locationParts[0];
String city = locationParts[1];
String country = locationParts[2];
Location location = new Location(address, city, country);
return location;
}
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
Location location = (Location) value;
return location.getAddress() + ", " +
location.getCity() + ", " +
location.getCountry();
}
}
在流程验证阶段(或应用请求值阶段)调用的 getAsObject()方法中,来自输入字符串的转换被转换为 location 对象,其类在清单 3-6 中提到,如粗体行所示,如果输入字符串不符合 Location 格式规范,则抛出一个 ConverterException,并显示一条 faces 错误消息。位置格式被指定为以下形式:
Address, City, Country
在渲染响应阶段调用的 getAsString()方法中,执行从位置对象到输出渲染字符串的转换。注意@FacesConverter 注释很重要,它用于在 JSF 应用中注册转换器。 @FacesConverter 注释有两个主要属性:value()属性,作为转换器 ID;for class()属性,作为类的转换器。在本例中,我们只使用了 value()属性,并将转换器 ID 声明为“com . jsfprohtml 5 . location converter”。
不使用@FacesConverter 批注,您可以在 JSF faces-config.xml 文件中声明转换器,如下所示:
<faces-config ...>
<converter>
<converter-id>com.jsfprohtml5.LocationConverter</converter-id>
<converter-class>com.jsfprohtml5.example.converters.LocationConverter</converter-class>
</converter>
</faces-config>
现在,让我们看看如何在 JSF 应用中使用 LocationConverter。清单 3-8 显示了 TestBean 管理的 bean ,它包括一个位置属性(Location)。
清单 3-8。 TestBean 托管 Bean 类
package com.jsfprohtml5.example.model;
import java.io.Serializable;
import java.util.Date;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class TestBean implements Serializable {
// ...
private Location location;
// ...
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
// ...
public String proceed() {
return null;
}
}
清单 3-9 显示了 LocationConverter 转换器的 XHTML 测试页面。
***清单 3-9。***location converter XHTML 测试页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Location Converter Test</title>
</h:head>
<h:body>
<h:form>
<h1>Converter Test</h1>
<h:outputText value="Enter location: "/>
<h:inputText id="location"
value="#{testBean.location}"
required="true">
<f:converter converterId="com.jsfprohtml5.LocationConverter" />
</h:inputText>
<br/>
<h:commandButton action="#{testBean.proceed}" value="Proceed"/><br/>
<h:outputText value="#{testBean.location}">
<f:converter converterId="com.jsfprohtml5.LocationConverter" />
</h:outputText>
<h:messages style="color: red"/>
</h:form>
</h:body>
</html>
如粗体行所示,在转换器测试页面中,有一个输入文本和一个输出文本,它们使用了 LocationConverter 的标签。标签是一个核心标签,主要是为了避免为每个自定义转换器创建 TLD(标签库描述符)而设计的。将自定义转换器的 ID(在@FacesConverter 注释中定义)传递给标记的 converterId 属性会将自定义转换器附加到父 ValueHolder 组件。
图 3-2 显示了当用户输入不符合位置(地址、城市、国家)格式的无效位置信息时将显示的转换错误。
图 3-2 。LocationConverter 错误消息
图 3-3 显示了当用户输入符合位置(地址、城市、国家)格式的有效位置信息时,LocationConverter 的行为。
图 3-3 。输入和输出文本的 LocationConverter 行为
验证
为了理解 JSF 验证,我们需要知道四个主题:验证器接口 API,标准的 JSF 验证器,如何在 JSF 构建一个定制的验证器,以及最后如何使用 Java Bean 验证(JSR 303)API。接下来的小节将详细阐述这些主题。
验证器接口
Java x . faces . validator . validator 接口是 JSF 验证器的核心接口。JSF 验证器接口描述了一个可以对 EditableValueHolder 组件执行验证(检查正确性)的 Java 类。单个 EditableValueHolder 在视图中可以有零个或多个验证器。清单 3-10 显示了 JSF 验证器接口。
清单 3-10。 JSF 验证器界面
package javax.faces.validator;
import java.util.EventListener;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
public interface Validator extends EventListener {
//...
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException;
}
如验证器接口所示,它包含一个 API。validate API 对父 EditableValueHolder 组件的值执行所需的验证;这个 API 可以在 流程验证阶段中调用(或者在应用请求值阶段中调用,如果 EditableValueHolder 的 immediate 属性设置为 true)。validate() API 有三个参数:
- 上下文,表示此请求的 JSF FacesContext 实例。
- 组件,它表示其值将被验证的组件。如果验证器需要使用组件属性,可以使用这个组件实例来检索组件属性。
- 值,它表示要验证的字符串值。
对于每个有验证器(或更多)的组件。如果验证失败,验证器必须抛出 ValidatorException 在这种情况下,拥有验证器的组件将被标记为无效,ValidatorException 消息将被接收并添加到 FacesContext 消息中,以便由与 EditableValueHolder 组件关联的< h:message >组件和< h:messages >组件显示。
清单 3-11 展示了一个例子来说明如何对多个带有验证器的 EditableValueHolder 组件进行验证。请注意,在本例中,number1 和 number2 是 TestBean 管理的 Bean 中的属性,属于 Long 类型。
清单 3-11。 多个 EditableValueHolder 组件的验证示例
<h:form>
<h:outputText value="Enter Number1: "/>
<h:inputText id="number1"
value="#{testBean.number1}">
<f:validateRequired/>
<f:validateLongRange minimum="0" maximum="999"/>
</h:inputText>
<h:message for="number1"/>
<br/>
<h:outputText value="Enter Number2: "/>
<h:inputText id="number2"
required="true"
value="#{testBean.number2}">
<f:validateLongRange minimum="0" maximum="999"/>
</h:inputText>
<h:message for="number2"/>
<br/>
<h:commandButton value="submit"/>
</h:form>
我们有一个包含两个 inputText 组件(number1 和 number2)的表单。每个 inputText 组件都有两个附加的验证器。每个输入文本都有以下验证器:
- < f:validateRequired >,用于验证 EditableValueHolder 组件不会包含空输入(使用 required="true "属性效果相同)。
- < f:validateLongRange >,用于验证长整型字段的值是否在指定的范围内(最小值和最大值)。
在示例中用于验证两个输入文本的最小值都等于 0,最大值都等于 999。如果用户在两个 inputText 组件中输入空值或超出范围的值,然后单击“提交”命令按钮,用户将在两个组件中看到两个与每个 inputText 组件相关联的验证错误消息。这解释了我们之前所说的,JSF 中的验证(像转换)必须被应用到每一个有一个或多个验证器的组件。
标准 JSF 验证器
现在,让我们深入 JSF 标准验证器的细节。表 3-2 显示了 JSF 标准验证器。
表 3-2 。JSF 标准验证器
|
验证器标签
|
描述
|
| — | — |
| <f:validateRequired>
| 用于验证EditableValueHolder
(如输入文本)值是必需的。设置required="true"
属性具有相同的效果。 |
| <f:validateLongRange>
| 用于验证长整型的EditableValueHolder
值是否在指定范围内。 |
| <f:validateDoubleRange>
| 用于验证双精度的EditableValueHolder
值是否在指定范围内。 |
| <f:validateLength>
| 用于验证EditableValueHolder
值是否在指定的长度范围内。 |
| <f:validateRegex>
| 用于验证EditableValueHolder
值是否符合指定的 Java 正则表达式。 |
| <f:validateBean>
| 用于将EditableValueHolder
本地值验证分配给 Java Bean 验证(JSR 303)API。 |
我们已经在清单 3-11 中看到了和验证器的例子。< f:validateDoubleRange >与< f:validateLongRange >相同;然而,它与 Double 而不是 Long 一起工作。< f:validateLength >验证器用于验证 EditableValueHolder 值是否在指定的长度范围内。例如:
<h:inputText id="address"
required="true"
value="#{person.address}">
<f:validateLength minimum="20" maximum="120"/>
</h:inputText>
如本例所示,在“address”输入文本中使用验证器验证 person managed bean 的 address 属性,其长度最少为 20 个字符,最多为 120 个字符。
validator 用于验证 EditableValueHolder 值是否符合指定的 Java 正则表达式。
<h:inputText id="email"
required="true"
value="#{person.email}">
<f:validateRegex pattern="(.+@.+\.[a-zA-Z]+)?"/>
</h:inputText>
如本例所示,使用“(”验证 person managed bean 的 email 属性是否具有有效的电子邮件。+@.+.[a-zA-Z]+)?”“email”输入文本的验证器的模式字段中的正则表达式。
validator 用于将 EditableValueHolder 本地值验证分配给 Java Bean 验证 API(JSR 303)。我们将在“用 JSF 验证 JSR 303 Bean”一节中详细介绍这个验证器。
您可以对必填字段验证错误消息使用 EditableValueHolder 的 requiredMessage 属性,或者对 EditableValueHolder 上的常规验证错误消息使用 validatorMessage 属性来覆盖验证消息。例如:
<h:inputText id="someNumber"
value="#{bean.someNumber}"
required="true"
requiredMessage="You have to enter a number"
validatorMessage="Number has to be minimum 10 and maximum 100">
<f:validateLongRange minimum="10" maximum="100"/>
</h:inputText>
如果用户没有在输入文本中输入值,将显示必填字段验证错误消息“您必须输入一个数字”,如果用户输入的数字超出范围(小于 10 或大于 100),将显示验证消息“数字必须最小为 10,最大为 100”。
构建自定义 JSF 验证器
除了 JSF 框架提供的所有提到的内置验证器,JSF 允许开发者创建他们自己的定制验证器。让我们看一个例子来说明这一点。假设我们想要一个 EmailValidator 自定义验证器,它验证用户输入是否符合电子邮件格式。
为了实现我们的 EmailValidator 自定义验证器,我们需要扩展验证器接口并实现 validate 方法。清单 3-12 展示了 EmailValidator 的实现。
清单 3-12。 EmailValidator 类
package com.jsfprohtml5.example.validators;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.FacesValidator;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
@FacesValidator("com.jsfprohtml5.EmailValidator")
public class EmailValidator implements Validator {
private static final String EMAIL_REGEX = "(.+@.+\\.[a-zA-Z]+)?";
private Pattern pattern;
private Matcher matcher;
public EmailValidator() {
pattern = Pattern.compile(EMAIL_REGEX);
}
@Override
public void validate(FacesContext context,
UIComponent component,
Object value)
throws ValidatorException {
matcher = pattern.matcher(value.toString());
if (! matcher.matches()) {
FacesMessage message = new FacesMessage("Invalid Email format",
"Use for example: xyz@company.com");
message.setSeverity(FacesMessage.SEVERITY_ERROR);
throw new ValidatorException(message);
}
}
}
在流程验证阶段(或应用请求值阶段)调用的 validate()方法中,会发生验证,如粗体行所示,如果输入字符串不符合电子邮件格式,则会抛出一个 ValidatorException 和一条 Faces 错误消息。注意@FacesValidator 注释很重要,它用于注册验证器。@FacesValidator 注释有一个主要属性,即 value()属性,它被视为验证器的 ID。对于本例,我们使用 value()属性,并将验证器 ID 声明为“com.jsfprohtml5.EmailValidator”。
不使用@FacesValidator 批注,您可以在 JSF faces-config.xml 文件中声明验证器,如下所示:
<faces-config ...>
...
<validator>
<validator-id>com.jsfprohtml5.EmailValidator</validator-id>
<validator-class>com.jsfprohtml5.example.validators.EmailValidator</validator-class>
</validator>
...
</faces-config>
现在,让我们看看如何在 JSF 应用中使用 EmailValidator。清单 3-13 显示了 TestBean 管理的 Bean 的更新版本(最初显示在清单 3-8 中),它包括一个类型为(String)的 email 属性。
***清单 3-13。***test Bean 托管 Bean 的更新版本
public class TestBean implements Serializable {
// ...
private String email;
// ...
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
// ...
}
清单 3-14 显示了 EmailValidator 验证器 XHTML 测试页面。
清单 3-14。 EmailValidator XHTML 测试页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Email Validator Test</title>
</h:head>
<h:body>
<h:form>
<h1>Validator Test</h1>
<h:outputText value="Enter Email: "/>
<h:inputText id="email"
value="#{testBean.email}" required="true">
<f:validator validatorId="com.jsfprohtml5.EmailValidator"/>
</h:inputText>
<br/>
<h:commandButton action="#{testBean.proceed}" value="Proceed"/><br/>
<h:messages style="color: red"/>
</h:form>
</h:body>
</html>
如粗体行所示,在测试页面中,有一个使用 EmailValidator 和标签的输入文本。标签是一个核心标签,主要是为了避免为每个定制验证器创建一个 TLD 而设计的。将@FacesValidator 注释中定义的自定义验证器的 ID 传递给标记的 validatorId 属性,会将自定义验证器附加到父 EditableValueHolder 组件。
图 3-4 显示了当用户输入不符合电子邮件格式要求的无效电子邮件时将显示的验证错误。
图 3-4 。EmailValidator 错误消息
图 3-5 显示了当用户输入符合电子邮件格式要求的有效电子邮件信息时,EmailValidator 的行为。
图 3-5 。EmailValidator 有效表单
JSR 303 与 JSF 比恩验证
Java EE 6 中引入了 JSR 303 Bean 验证,以便在 Java 企业应用中支持构建验证。JSR 303 自带内置的验证器(约束),比如@NotNull、@Min、@Max、@Past、@Future、@Size 等。,它还允许在应用域模型上(或者通常在 POJO 上)创建自定义约束。除了 JSF 内置和自定义验证器,从 JSF 2.0 开始,JSF 和 JSR 303 Java Bean 验证 API 之间有了默认的集成。在接下来的小节中,这种集成将在一个示例订阅者应用中进行说明。
在订阅应用中,用户可以保存姓名、地址和电子邮件进行订阅,如图图 3-6 所示。
图 3-6 。订户应用
用户信息验证如下:
- 所有字段都是必填的。
- 用户名必须至少为 4 个字符,最多为 30 个字符。
- 地址必须至少为 12 个字符,最多为 120 个字符。
- 电子邮件必须有效。
从技术上讲,我们将使用 JSF 必填字段验证器来实现第一个验证需求。所有其他验证都将使用 JSR 303 API 来实现。在验证需求 2 和 3 中,将使用 JSR 303 @Size 注释,而对于需求 4,我们将实现一个定制的 JSR 303 约束。清单 3-15 显示了一个在应用主 XHTML 页面中使用的个人管理 bean。
清单 3-15。 人管豆
package com.jsfprohtml5.subscriber.model;
import com.jsfprohtml5.subscriber.bean.validation.custom.EmailAddress;
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.validation.constraints.Size;
@ManagedBean
@RequestScoped
public class Person implements Serializable {
@Size(min = 4, max = 30)
private String name;
@Size(min = 12, max = 120)
private String address;
@EmailAddress
private String email;
public Person() {
}
public String subscribe() {
return null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
如粗体行所示,为了控制姓名和地址字段的大小,我们使用了@size 内置 Java Bean 验证注释。@size 注释主要有两个属性,min 和 max,用来验证注释字段的长度。@EmailAddress 注释是一个自定义约束,我们用它来验证 Person managed bean 的 email 属性。清单 3-16 显示了@EmailAddress 注释的代码。
清单 3-16。 @EmailAddress 注释
package com.jsfprohtml5.subscriber.bean.validation.custom;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(validatedBy = EmailValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EmailAddress {
String message() default "{email.invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
如果您熟悉 Java 注释,使用@接口,您可以创建一个注释类型。@Target annotation 表示注释类型适用的 Java 程序元素(在本例中,这些元素是 Java 字段和方法)。Java Bean 验证规范(JSR 303)要求任何约束注释都要定义以下属性:
- 消息属性,默认情况下应该返回错误消息。它可以返回实际的错误信息文本,也可以通过使用如下的花括号“{key}”来返回错误信息密钥。在前面的代码清单中,它返回 email.invalid key。
- groups 属性,允许指定该约束所属的验证组。
- payload 属性,Java Bean Validation API 的客户端可以使用它将定制的有效负载对象分配给一个约束(超出了本书的范围)。
@Constraint 批注是一个 Java Bean 验证批注,它引用使用 validatedBy 属性执行验证逻辑的类。清单 3-17 展示了 EmailValidator 验证类的实现。
清单 3-17。 EmailValidator 验证类
package com.jsfprohtml5.subscriber.bean.validation.custom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EmailValidator implements ConstraintValidator<EmailAddress, String> {
private static final String EMAIL_REGEX = "(.+@.+\\.[a-zA-Z]+)?";
private Pattern pattern;
private Matcher matcher;
@Override
public void initialize(EmailAddress constraintAnnotation) {
pattern = Pattern.compile(EMAIL_REGEX);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
matcher = pattern.matcher(value);
if (! matcher.matches()) {
return false;
}
return true;
}
}
正如你会注意到的,EmailValidator 约束与我们在清单 3-12 中开发的 JSF EmailValidator 类具有相同的逻辑。然而,有一个主要的区别:在 JSF 验证器中,当验证失败时,验证器抛出异常,但是在 Java Bean 验证中,验证器返回 false。initialize()方法用于初始化自定义约束;值得注意的是,这个方法保证在任何其他约束实现方法之前被调用。
现在,在构建了自定义约束之后,我们就可以在我们的 JSF XHTML 页面中使用内置和自定义约束了。清单 3-18 显示了订阅者应用主页(index.xhtml)。
清单 3-18。 订阅者应用 XHTML 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<ui:composition template="/WEB-INF/templates/default.xhtml">
<ui:define name="title">
#{bundle['application.subscriber.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.name']}"></h:outputText>
<h:inputText id="userName"
value="#{person.name}"
required="true"
requiredMessage="#{bundle['user.name.required']}">
</h:inputText>
<h:message for="userName" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.address']}"></h:outputText>
<h:inputText id="address"
value="#{person.address}"
required="true"
requiredMessage="#{bundle['user.address.required']}">
</h:inputText>
<h:message for="address" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.email']}"></h:outputText>
<h:inputText id="email"
value="#{person.email}"
required="true"
requiredMessage="#{bundle['user.email.required']}">
</h:inputText>
<h:message for="email" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.subscribe']}"
action="#{person.subscribe}">
</h:commandButton>
<br/>
<h:messages styleClass="errorMessage"/>
</h:form>
</ui:define>
</ui:composition>
</html>
正如我们在代码清单中注意到的,我们没有做什么特别的事情;我们对所有字段使用了 JSF 必需字段验证器。值得注意的是,所有内置或定制的 JSR 303 验证器(@Size 和@EmailValidator)将自动应用于 JSF 组件,而无需任何额外的步骤。如图 3-7 所示,Java Bean 验证错误会自动附加到 JSF < h:message >和< h:messages >组件上。
图 3-7 。JSF 消息组件中的 Java Bean 验证错误
默认情况下,所有的 Java Bean 验证(JSR 303)验证器将在 JSF 管理的 Bean 中自动启用;为了禁用此行为,可以将 web.xml 中的 javax . faces . VALIDATOR . disable _ DEFAULT _ BEAN _ VALIDATOR 上下文参数设置为 true,如下所示:
<context-param>
<param-name>javax.faces.validator.DISABLE_DEFAULT_BEAN_VALIDATOR</param-name>
<param-value>true</param-value>
</context-param>
值得注意的一点是,Java Bean Validation 是一个独立的验证框架,它不是 JavaServer Faces 框架的一部分(尽管它们之间有很好的集成,正如我们在订阅者应用中看到的);这意味着您需要为 Java Bean 验证消息提供一个单独的属性文件。根据 JSR 303,这个属性文件应该命名为 ValidationMessages.properties,并带有处理不同语言环境的语言环境变量,Java Bean 验证属性文件应该放在 JSF 应用的默认包(类路径的根)下。
注涵盖 JSR 303 的全部特性不在本书讨论范围之内;你可以在 http://jcp.org/en/jsr/detail?id=303 的阅读完整的 JSR 303 规范。
清单 3-15 中的所有验证字段都属于 Java Bean 验证框架的默认验证组;但是,您可以选择为同一个约束条件指定多个验证组。验证组只不过是一个标记接口。让我们创建两个名为(LengthGroup)和(EmailGroup)的验证组。LengthGroup 将在个人管理的 bean 中对长度约束进行分组(@Size constraints),而 EmailGroup 将在个人管理的 bean 中包含电子邮件约束(@EmailAddress constraint)。然后将验证组附加到约束上,如清单 3-19 所示。
清单 3-19。 亲自验证团体托管豆
@ManagedBean
@RequestScoped
public class Person implements Serializable {
@Size(min = 4, max = 30, groups = LengthGroup.class)
private String name;
@Size(min = 12, max = 120, groups = LengthGroup.class)
private String address;
@Size(min = 5, max = 30, groups = LengthGroup.class)
@EmailAddress(groups = EmailGroup.class)
private String email;
public Person() {
}
public String subscribe() {
return null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
默认情况下,JSF 执行分组在默认验证组下的约束。为了在 EditableValueHolder 上运行特定的验证组,JSF 提供了一个标签,可以用来选择在父 EditableValueHolder 上执行哪些验证组。这个特性对输入字段验证的级别提供了很好的控制。清单 3-20 显示了订阅者应用更新后的 index.xhtml。
***清单 3-20。***更新 index.xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<ui:composition template="/WEB-INF/templates/default.xhtml">
<ui:define name="title">
#{bundle['application.subscriber.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.name']}"></h:outputText>
<h:inputText id="userName"
value="#{person.name}"
required="true"
requiredMessage="#{bundle['user.name.required']}">
<f:validateBean
validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup"/>
</h:inputText>
<h:message for="userName" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.address']}"></h:outputText>
<h:inputText id="address"
value="#{person.address}"
required="true"
requiredMessage="#{bundle['user.address.required']}">
<f:validateBean
validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup"/>
</h:inputText>
<h:message for="address" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.email']}"></h:outputText>
<h:inputText id="email"
value="#{person.email}"
required="true"
requiredMessage="#{bundle['user.email.required']}">
<f:validateBean
validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup,
com.jsfprohtml5.subscriber.bean.validation.groups.EmailGroup"/>
</h:inputText>
<h:message for="email" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.subscribe']}"
action="#{person.subscribe}">
</h:commandButton>
<br/>
<h:messages styleClass="errorMessage"/>
</h:form>
</ui:define>
</ui:composition>
</html>
如粗体行所示,标签可以附加到 EditableValueHolders,上,并使用 validationGroups 属性,您可以指定要在父 EditableValueHolder 上执行的约束类的全限定名称(用逗号分隔)。
注订阅者应用的完整 Maven 项目包含在本书网页【apress.com】第三章 资源下。
覆盖标准消息
为了获得更好的用户体验,覆盖标准转换和验证错误消息总是有用的。虽然您可以通过使用 EditableValueHolder 组件的 validatorMessage、requiredMessage 和 converterMessage 属性来自定义转换和验证错误消息,但与在每个 EditableValueHolder 组件上相比,在应用级别上全局覆盖 JSF 标准转换和验证消息需要更少的工作量和更高的准确性。
为了全局覆盖 JSF 标准转换和验证消息,您必须使用标准消息关键字来覆盖应用消息包中的消息。表 3-3 显示了根据 JSF 规范的可能的 JSF 标准消息键。
表 3-3 。根据 JSF 规范的 JSF 标准消息密钥
|
钥匙
|
默认消息
|
| — | — |
| javax.faces.component.UIInput.CONVERSION
| {0}:出现转换错误。 |
| javax.faces.component.UIInput.REQUIRED
| {0}:验证错误:值是必需的。 |
| javax.faces.component.UIInput.UPDATE
| {0}:处理您提交的信息时出错。 |
| javax.faces.component.UISelectOne.INVALID
| {0}:验证错误:值无效 |
| javax.faces.component.UISelectMany.INVALID
| {0}:验证错误:值无效 |
| javax.faces.converter.BigDecimalConverter.DECIMAL
| { 2 }:“{ 0 }”必须是有符号的十进制数。 |
| javax.faces.converter.BigDecimalConverter.DECIMAL_detail
| { 2 }:“{ 0 }”必须是由零个或多个数字组成的有符号十进制数,后面可以跟一个小数点和分数。示例:{1} |
| javax.faces.converter.BigIntegerConverter.BIGINTEGER
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。javax . faces . converter . biinteger converter . big integer _ detail = { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。示例:{1} |
| javax.faces.converter.BooleanConverter.BOOLEAN
| { 1 }:“{ 0 }”必须为“true”或“false”。 |
| javax.faces.converter.BooleanConverter.BOOLEAN_detail
| { 1 }:“{ 0 }”必须为“true”或“false”。除“true”之外的任何值都将计算为“false”。 |
| javax.faces.converter.ByteConverter.BYTE
| { 2 }:“{ 0 }”必须是 0 到 255 之间的数字。 |
| javax.faces.converter.ByteConverter.BYTE_detail
| { 2 }:“{ 0 }”必须是 0 到 255 之间的数字。示例:{1} |
| javax.faces.converter.CharacterConverter.CHARACTER
| { 1 }:“{ 0 }”必须是有效字符。 |
| javax.faces.converter.CharacterConverter.CHARACTER_detail
| { 1 }:“{ 0 }”必须是有效的 ASCII 字符。 |
| javax.faces.converter.DateTimeConverter.DATE
| { 2 }:“{ 0 }”无法被理解为日期。 |
| javax.faces.converter.DateTimeConverter.DATE_detail
| { 2 }:“{ 0 }”无法被理解为日期。示例:{1} |
| javax.faces.converter.DateTimeConverter.TIME
| { 2 }:“{ 0 }”无法被理解为时间。 |
| javax.faces.converter.DateTimeConverter.TIME_detail
| { 2 }:“{ 0 }”无法被理解为时间。示例:{1} |
| javax.faces.converter.DateTimeConverter.DATETIME
| { 2 }:“{ 0 }”无法被理解为日期和时间。 |
| javax.faces.converter.DateTimeConverter.DATETIME_detail
| { 2 }:“{ 0 }”无法被理解为日期和时间。示例:{1} |
| javax.faces.converter.DateTimeConverter.PATTERN_TYPE
| {1}:必须指定“pattern”或“type”属性来转换值“{0}”。 |
| javax.faces.converter.DoubleConverter.DOUBLE
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.DoubleConverter.DOUBLE_detail
| { 2 }:“{ 0 }”必须是介于 4.9E-324 和 1.7976931348623157E308 之间的数字,例如:{1} |
| javax.faces.converter.EnumConverter.ENUM
| { 2 }:“{ 0 }”必须可转换为枚举。 |
| javax.faces.converter.EnumConverter.ENUM_detail
| { 2 }:“{ 0 }”必须可以从包含常数“{1}”的枚举转换为枚举。 |
| javax.faces.converter.EnumConverter.ENUM_NO_CLASS
| { 1 }:“{ 0 }”必须可从枚举转换为枚举,但未提供枚举类。 |
| javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail
| { 1 }:“{ 0 }”必须可从枚举转换为枚举,但未提供枚举类。 |
| javax.faces.converter.FloatConverter.FLOAT
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.FloatConverter.FLOAT_detail
| { 2 }:“{ 0 }”必须是介于 1.4E-45 和 3.4028235E38 之间的数字,例如:{1} |
| javax.faces.converter.IntegerConverter.INTEGER
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.IntegerConverter.INTEGER_detail
| { 2 }:“{ 0 }”必须是介于–2147483648 和 2147483647 之间的数字。示例:{1} |
| javax.faces.converter.LongConverter.LONG
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.LongConverter.LONG_detail
| { 2 }:“{ 0 }”必须是介于–9223372036854775808 和 9223372036854775807 之间的数字。示例:{1} |
| javax.faces.converter.NumberConverter.CURRENCY
| { 2 }:“{ 0 }”无法被理解为货币值。 |
| javax.faces.converter.NumberConverter.CURRENCY_detail
| { 2 }:“{ 0 }”无法被理解为货币值。示例:{1} |
| javax.faces.converter.NumberConverter.PERCENT
| { 2 }:“{ 0 }”无法理解为百分比。 |
| javax.faces.converter.NumberConverter.PERCENT_detail
| { 2 }:“{ 0 }”无法理解为百分比。示例:{1} |
| javax.faces.converter.NumberConverter.NUMBER
| { 2 }:“{ 0 }”不是一个数字。 |
| javax.faces.converter.NumberConverter.NUMBER_detail
| { 2 }:“{ 0 }”不是一个数字。示例:{1} |
| javax.faces.converter.NumberConverter.PATTERN
| { 2 }:“{ 0 }”不是数字模式。 |
| javax.faces.converter.NumberConverter.PATTERN_detail
| { 2 }:“{ 0 }”不是数字模式。示例:{1} |
| javax.faces.converter.ShortConverter.SHORT
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.ShortConverter.SHORT_detail
| { 2 }:“{ 0 }”必须是介于–32768 和 32767 之间的数字例如:{1} |
| javax.faces.converter.STRING
| {1}:无法将“{0}”转换为字符串。 |
| javax.faces.validator.BeanValidator.MESSAGE
| {0} |
| javax.faces.validator.DoubleRangeValidator.MAXIMUM
| {1}:验证错误:值大于允许的最大值“{0}”。 |
| javax.faces.validator.DoubleRangeValidator.MINIMUM
| {1}:验证错误:值小于允许的最小值“{0}”。 |
| javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE
| {2}:验证错误:指定的属性不在{0}和{1}的预期值之间。 |
| javax.faces.validator.DoubleRangeValidator.TYPE
| {0}:验证错误:值的类型不正确。 |
| javax.faces.validator.LengthValidator.MAXIMUM
| {1}:验证错误:长度大于允许的最大值“{0}”。 |
| javax.faces.validator.LengthValidator.MINIMUM
| {1}:验证错误:长度小于允许的最小值“{0}”。 |
| javax.faces.validator.LongRangeValidator.MAXIMUM
| {1}:验证错误:值大于允许的最大值“{0}”。 |
| javax.faces.validator.LongRangeValidator.MINIMUM
| {1}:验证错误:值小于允许的最小值“{0}”。 |
| javax.faces.validator.LongRangeValidator.NOT_IN_RANGE
| {2}:验证错误:指定的属性不在{0}和{1}的预期值之间。 |
| javax.faces.validator.LongRangeValidator.TYPE
| {0}:验证错误:值的类型不正确。 |
让我们看一个例子,看看如何进行这种定制。下面的代码片段显示了一个输入文本,该文本被验证为必需的,并且值在 10 到 100 之间。
<h:inputText id="someNumber"
value="#{testBean.number}"
required="true">
<f:validateLongRange minimum="10" maximum="100"/>
</h:inputText>
<br/>
<h:commandButton action="#{testBean.proceed}" value="Proceed"/><br/>
当用户输入超出范围的数字,然后单击“继续”命令按钮时,将向用户显示以下 JSF 默认验证错误消息:
xxx:someNumber: Validation Error: Specified attribute is not between the expected values of 10 and 100.
为了改变信息,我们需要做以下工作:
-
覆盖应用消息包中的 javax . faces . validator . longrangevalidator . not _ IN _ RANGE 键,如下所示:
javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = {2}''s value must be minimum {0} and maximum {1}.
-
在 faces-config.xml 文件中注册应用消息包,如下所示:
<faces-config ...> ... <application> <message-bundle>com.jsfprohtml5.application.Messages</message-bundle> </application> ... </faces-config>
在应用的消息包和 faces-config.xml 文件中完成这些更改之后,在输入文本中输入一个超出范围的数字,然后单击“proceed”命令按钮,最终的错误消息将是
xxx:someNumber's value must be minimum 10 and maximum 100.
摘要
在这一章中,你详细地学习了 JSF 验证和转换。您知道 JSF 转换器、JSF 转换器接口和 JSF 标准转换器的生命周期,并且了解如何构建定制的 JSF 转换器。您还学习了 JSF 验证、JSF 验证器接口和 JSF 标准验证器的生命周期;如何构建自定义 JSF 验证器;最后,如何在 JSF 应用中使用 Java Bean 验证(JSR 303)。在本章的最后,您学习了如何通过定制 JSF 框架标准转换和验证消息来增强您的 JSF 应用。
四、引擎盖下的 JSF——第三部分
在本章中,您将通过理解 JSF 事件模型,详细了解如何增强您的 JSF 应用。完成本章后,您将了解不同的 JSF 事件类型(JSF 面孔事件、相位事件和系统事件)。您将学习如何在 JSF 应用中处理 JSF 事件。在最后一节中,您将学习如何利用 JSF 视图参数来生成 RESTful JSF 页面,这些页面可以被最终用户添加为书签,也可以被 web 搜索爬虫编入索引。
JSF 事件
在深入 JSF 事件的细节之前,我们需要先了解什么是事件和什么是事件监听器。事件通常是用户执行的操作(如单击按钮或更改下拉值)。当事件发生时,事件源对象中的更改(或一组更改)会发生,并被事件对象捕获。事件对象应该告诉事件的源对象是什么,以及事件源发生了什么变化(如果有的话)。事件侦听器通常是一个在特定事件(事件侦听器类感兴趣的事件)发生时必须得到通知的类。
通常,在 Java 世界中,事件模型中的两个主要组件被表示为一个接口和一个类,如图图 4-1 所示。
图 4-1 。Java 事件模型主接口
EventListener 是一个标记接口,没有所有事件侦听器接口都必须扩展的方法,而 EventObject 类主要有一个方法 getSource(),它返回事件最初发生的对象(事件源)。为了处理 Java 事件模型,JSF 利用 EventObject 和 EventListener 来构建它的事件和侦听器模型。
为了定义 JSF 监听器,所有的 JSF 监听器接口都扩展了 EventListener 接口。图 4-2 显示了主要的 JSF 事件监听器(注意,为了简单起见,这个图没有显示 JSF 监听器的完整列表)。
图 4-2 。JSF 事件听众
有两种类型的 JSF 事件监听器:
- Faces 监听器是动作监听器、值更改监听器、系统事件监听器和组件系统事件监听器的基本接口。
- 阶段监听器是监听器接口,用于 JSF 请求处理生命周期的每个标准阶段的开始和结束。
让我们深入了解子侦听器的细节,以了解谁可以创建这些侦听器以及何时可以执行它们。图 4-3 显示了两种与应用相关的事件监听器:
- ActionListener 接口,负责接收动作事件。
- ValueChangeListener 接口,负责接收值变化事件。
图 4-3 。与应用相关的事件侦听器
如此图所示,ActionSource 组件(或 ActionSource2 组件)可以有一个或多个操作侦听器。EditableValueHolder 组件(如 UIInput 组件)或 ValueHolder 组件可能有一个或多个值更改侦听器。
根据定义,系统事件监听器监听系统事件。系统事件是在 JSF 2.0 中引入的,它为 JSF 生命周期提供了一个优雅的视图。例如,使用系统事件侦听器,JSF 开发人员可以编写自定义代码,这些代码将在应用启动和拆卸事件中执行,或者在应用中引发异常时执行。
SystemEventListener 是系统事件侦听器的主接口。SystemEventListener 可以侦听所有系统事件类型。系统事件可以在 JSF 应用级别触发(如应用启动、应用拆除或应用异常),也可以在 JSF 组件级别触发(如“验证组件之前”、“验证组件之后”或“呈现视图之前”)。如果系统事件发生在组件级,那么在这种情况下你可以使用更具体的事件监听器,这是 ComponentSystemEventListener 接口(如图图 4-2 所示)来处理。这是因为 ComponentSystemEventListener 侦听 ComponentSystemEvent(它扩展了 SystemEvent)。
阶段监听器允许处理不同的阶段事件。阶段事件发生在 JSF 请求处理生命周期的每个标准阶段的开始和结束。JSF 请求处理生命周期阶段有
- 恢复视图。
- 应用请求值。
- 流程验证。
- 更新模型值。
- 调用应用。
- 渲染响应。
如图 4-4 所示,生命周期实例可以有零个或多个附加的 phase listener,UIViewRoot 可以有零到两个 phase listener 实例。阶段侦听器将在“阶段事件”一节中详细说明。
图 4-4 。PhaseListener 接口
注意还有一个监听器扩展了 FacesListener(为了简单起见,在图 4-2 中省略了):这个监听器是 BehaviorListener。BehaviorListener 监听 JSF HTML 组件的所有行为事件。这些事件将在第五章中说明。
既然我们已经介绍了 JSF 事件监听器模型,那么 JSF 事件模型呢?图 4-5 显示了 JSF 事件层级中的主要类别。
图 4-5 。JSF 事件对象层次结构中的主要类
为了定义 JSF 事件对象,所有的 JSF 事件对象都扩展了 EventObject 类。有三件重要的事情需要注意:
- FacesEvent 是可由 UIComponents 触发的事件的基类。它有两个子类:ActionEvent 类,表示由 ActionSource2 组件触发的事件 ValueChangeEvent 类,表示由 ValueHolder 或 EditableValueHolder 组件触发的事件。
- SystemEvent 是所有系统事件的基类。ComponentSystemEvent 类(扩展 SystemEvent)代表特定于 UIComponents 的系统事件。
- PhaseEvent 是一个类,表示在 JSF 请求处理生命周期的每个标准阶段的开始和结束时发生的阶段事件。
注意重要的是要知道所有的 JSF 事件监听器接口和事件类都位于(javax.faces.event)包中。在本章中,为了简单起见,我们将只提及事件监听器接口名或事件类名,而不提及完全限定的接口(或类)名。
面孔事件
Faces 事件 是那些可以被 UIComponents 触发的事件。面孔事件包括两种类型的事件:
- 动作事件。
- 价值变化事件。
动作事件由 ActionSource2 组件触发,如(CommandButton 或 CommandLink 组件)。例如,当单击 UICommand 组件时,将触发 action 事件。值更改事件由 ValueHolder 组件(如 outputText 组件)或 EditableValueHolder 组件(如 inputText 或 selectOneMenu 组件)触发。当组件的值更改时,将触发值更改事件。在讨论这两种类型事件的示例之前,重要的是要知道这些事件在 JSF 生命周期的各个阶段中的触发时间。图 4-6 显示了动作和数值变化事件的执行时间。
图 4-6 。动作和值更改事件的执行时间
如图所示,动作事件和值更改事件在以下三个阶段结束时执行:
- 应用请求值。
- 流程验证。
- 调用应用。
这是两个事件的四个执行场景 :
- 当 EditableValueHolder(或 ValueHolder)组件的 immediate 属性设置为 true 时,将在“应用请求值”阶段结束时执行 ValueChangeEvent。
- 当 ActionSource2 组件的 immediate 属性设置为 true 时,ActionEvent 将在“应用请求值”阶段结束时执行。
- 当 EditableValueHolder(或 ValueHolder)组件的 immediate 属性设置为 false 时,将在“流程验证”阶段结束时执行 ValueChangeEvent。
- 当 ActionSource2 组件的 immediate 属性设置为 false 时,将在“调用应用”阶段结束时执行 ActionEvent。
在接下来的两节中,我们将看到如何为动作和值更改事件创建侦听器的不同示例。
注意根据对执行场景的解释,需要注意的是,默认情况下(当 immediate 属性设置为 false 时),动作事件和值更改事件都会排队,这意味着一旦用户(例如)对 ActionSource2 组件执行动作或对 EditableValueHolder 组件的值进行更改,这些事件将不会被触发。这两个事件都将排队,直到它们在 JSF 请求处理生命周期中的适当时间被触发,如上图所示。
动作事件
在前面的章节中,我们看到了一些带有动作方法的动作事件和动作监听器的例子。我们来回忆一下第二章的第一个应用例子。清单 4-1 展示了在第一个应用示例中带有动作方法的动作监听器的例子。
清单 4-1。 “第一次应用”示例中带有动作方法的动作监听器示例
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['application.loginpage.title']}
</ui:define>
<ui:define name="content">
...
<h:commandButton value="#{bundle['application.login']}" action="#{user.login}"/>
<br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
正如我们在清单 4-1 中看到的,我们可以使用 UICommand (CommandButton) action 属性创建一个带有 action 方法的 action 监听器。清单 4-2 显示了#{user.login}动作方法代码。
清单 4-2。 #{user.login}动作方法代码
public class User implements Serializable {
...
public String login() {
return "welcome";
}
...
}
正如您将注意到的,action 方法是一种不带参数并返回表示结果的字符串的方法。
注意的动作方法是由 JSF 内置的默认动作监听器处理的。默认的操作侦听器从操作方法中获取返回的结果字符串,然后将其传递给 NavigationHandler 来处理导航(如果有)。
如果您的操作方法中不需要导航,那么您可以使用操作侦听器方法来代替。清单 4-3 显示了一个包含 CommandButton 和 action listener 方法的表单,用于计算输入字段的阶乘数。
清单 4-3。 一个动作监听器方法的例子
<h:form>
<h:outputText value="Enter Number:"/>
<h:inputText value="#{calc.number}">
<f:validateLongRange minimum="0" maximum="25"/>
</h:inputText>
<br/>
<h:commandButton value="Calculate Factorial"
actionListener="#{calc.findFactorial}">
</h:commandButton>
<br/>
<h:outputText value="Result is: #{calc.result}" rendered="#{calc.result ne 0}"/>
<h:messages/>
</h:form>
可以使用 UICommand 组件的 action listener 属性将操作侦听器方法附加到 ui command 组件。如前所述,操作侦听器方法执行操作而不返回 JSF 导航的任何结果,因此操作侦听器方法返回 void 并将 ActionEvent 作为参数。清单 4-4 显示了 Calc 管理的 bean,它包括 findFactorial 动作监听器。
清单 4-4。 Calc 托管 Bean
@ManagedBean
@RequestScoped
public class Calc implements Serializable {
private int number;
private long result;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public long getResult() {
return result;
}
public void setResult(long result) {
this.result = result;
}
public void findFactorial(ActionEvent event) {
result = 1;
for (int i = 1; i <= number; i++) {
result = result * i;
}
}
}
除了将默认操作侦听器与操作方法或操作侦听器方法一起使用之外,您还可以编写自己的自定义操作侦听器。这可以通过创建实现 ActionListener 接口的 action listener 类来实现。清单 4-5 展示了 CalcActionListener,它实现了 ActionListener。如图所示,processAction()获取当前输入数字,然后计算该数字的相应阶乘,最后在 Calc 托管 bean 的 result 属性中设置输出。
清单 4-5。 CalcActionListener 自定义动作监听器
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;
public class CalcActionListener implements ActionListener {
@Override
public void processAction(ActionEvent event) throws AbortProcessingException {
FacesContext context = FacesContext.getCurrentInstance();
Calc calc = context.getApplication().evaluateExpressionGet(context,
"#{calc}",
Calc.class);
long result = 1;
for (int i = 1; i <= calc.getNumber(); i++) {
result = result * i;
}
calc.setResult(result);
}
}
为了将定制动作监听器附加到 UICommand 组件,可以在 UICommand 组件中使用标记。清单 4-6 显示了对清单 4-3 中提到的表单的更新,包括定制动作监听器的更新。
***清单 4-6。***XHTML 页面中自定义动作监听器的例子
<h:form>
<h:outputText value="Enter Number:"/>
<h:inputText value="#{calc.number}">
<f:validateLongRange minimum="0" maximum="25"/>
</h:inputText>
<br/>
<h:commandButton value="Calculate Factorial">
<f:actionListener type="com.jsfprohtml5.factorial.model.CalcActionListener"/>
</h:commandButton>
<br/>
<h:outputText value="Result is: #{calc.result}" rendered="#{calc.number ne 0}"/>
<h:messages/>
</h:form>
要遵循的最佳实践是使用操作方法来执行业务操作,这可能还包括导航到新页面,并使用操作侦听器方法(或自定义操作侦听器)在执行实际的业务操作之前为操作做一些初始化工作(例如记录操作)。知道动作监听器方法(或自定义动作监听器)总是在动作方法之前执行是很重要的,执行顺序与它们在视图中声明和附加到 ActionSource2 组件的顺序相同。清单 4-7 显示了在清单 4-3 中提到的阶乘计算表单,它结合了“计算阶乘”命令按钮组件上的动作方法和动作监听器。
清单 4-7。 动作方法和动作监听器组合在“计算阶乘”命令组件上
<h:form>
<h:outputText value="Enter Number:"/>
<h:inputText value="#{calc.number}">
<f:validateLongRange minimum="0" maximum="25"/>
</h:inputText>
<br/>
<h:commandButton value="Calculate Factorial"
actionListener="#{calc.logFindFactorial}"
action="#{calc.findFactorial}">
</h:commandButton>
<br/>
<h:outputText value="Result is: #{calc.result}" rendered="#{calc.number ne 0}"/>
<h:messages/>
</h:form>
记录 FindFactorial 操作的# { calc.logFindFactorial }的执行将在# { calc.findFactorial }进行实际阶乘计算之前执行。清单 4-8 显示了最初在清单 4-4 中显示的 Calc 受管 bean 的更新。
清单 4-8。 更新了 Calc 托管 Bean
@ManagedBean
@RequestScoped
public class Calc implements Serializable {
private long number;
private long result;
public long getNumber() {
return number;
}
public void setNumber(long number) {
this.number = number;
}
public long getResult() {
return result;
}
public void setResult(long result) {
this.result = result;
}
public void logFindFactorial(ActionEvent event) {
System.out.println("Getting the factorial for: " + number);
}
public String findFactorial() {
result = 1;
for (int i = 1; i <= number; i++) {
result = result * i;
}
System.out.println("Factorial(" + number + ") = " + result);
return null;
}
}
在“数字”字段中输入一个数字,然后单击“计算阶乘”命令按钮,将在控制台中打印以下行,如下所示:
Getting the factorial for: 3
Factorial(3) = 6
有时,您可能需要在执行操作方法之前直接在托管 bean 属性中设置一个值;如果您遇到这种情况,那么您可以在 ActionSource2 组件中使用标记。表 4-1 显示了<f:setPropertyActionListener>标签的主要属性。
表 4-1 。<f:setPropertyActionListener>标签的主要属性
|
属性
|
描述
|
| — | — |
| 价值* | 表示要存储为target
属性的值的ValueExpression
。 |
| 目标* | 表示作为value
属性的目的地的ValueExpression
。 |
为了理解如何使用标签,让我们看一个例子。清单 4-9 展示了一个 CommandButton 中的<f:setPropertyActionListener>标签的例子。
清单 4-9。??<f:setPropertyActionListener>的一个例子
<h:commandButton value="Say Hi" action="page2">
<f:setPropertyActionListener target="#{person.name}" value="Some user"/>
</h:commandButton>
如前面的代码清单所示,当单击 CommandButton 时,Person managed bean 的 name 属性将被设置为“Some user ”,然后当前页面将被转到 page2。清单 4-10 显示了个人管理的 bean。
清单 4-10。 人管豆
@ManagedBean
@SessionScoped
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Person managed bean 是一个简单的 bean,只有一个属性名及其 setter 和 getter。在使用标记将 Person managed bean 的 name 属性设置为“Some user”之后,在 page2 中,如果我们在页面中有以下表达式:
Hello, #{person.name}
这将产生
Hello, Some user
数值变化事件
值更改事件是当 ValueHolder(或 EditableValueHolder)组件的值更改时触发的事件。让我们看一个值改变监听器的例子。假设我们想要显示一组国家的首都,当用户选择这些国家中的一个,然后单击一个命令按钮来获取所选国家的首都时,这些国家的首都显示在 JSF 的 SelectOneMenu 组件中。清单 4-11 显示了包含国家列表的表单。
清单 4-11。 价值改变听者的例子
<h:form>
<h:outputLabel for="countries" value="Select a country: "/>
<h:selectOneMenu id="countries" value="#{country.name}"
valueChangeListener="#{country.findCapital}">
<f:selectItem itemLabel="---" itemValue="---"/>
<f:selectItem itemLabel="United States" itemValue="USA"/>
<f:selectItem itemLabel="Egypt" itemValue="Egypt"/>
<f:selectItem itemLabel="Denmark" itemValue="Denmark"/>
</h:selectOneMenu>
<h:commandButton value="Find Capital" /> <br/>
<h:outputText value="Capital of #{country.name} is #{country.capital}"
rendered="#{country.capital ne null}"/>
</h:form>
正如我们在粗体行中看到的,我们有一个 selectOneMenu 组件,它有四个使用标签添加的项目。第一项表示没有选择,而其余项表示国家。selectOneMenu 组件具有 valueChangeListener 属性,该属性包含值更改侦听器方法#{country.findCapital}。当用户选择一个可用的国家,然后单击 CommandButton 时,如果 selectOneMenu 值发生更改,将提交表单并执行值更改侦听器方法。清单 4-12 显示了国家管理的 bean。
清单 4-12。 国家托管豆
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.event.ValueChangeEvent;
@ManagedBean
@RequestScoped
public class Country {
private String name;
private String capital;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCapital() {
return capital;
}
public void setCapital(String capital) {
this.capital = capital;
}
public void findCapital(ValueChangeEvent event) {
System.out.println("Old selected value is: " + event.getOldValue());
System.out.println("New selected value is: " + event.getNewValue());
String selectedCountryName = (String) event.getNewValue();
if ("USA".equals(selectedCountryName)) {
capital = "Washington";
} else if ("Egypt".equals(selectedCountryName)) {
capital = "Cairo";
} else if ("Denmark".equals(selectedCountryName)) {
capital = "Copenhagen";
}
}
}
正如您在粗体行中注意到的,值更改侦听器方法返回 void,并将 ValueChangeEvent 作为参数。使用 ValueChangeEvent 的 getOldValue()和 getNewValue()方法,可以获得 ValueHolder(或 EditableValueHolder)组件的旧值和新值。在我们的示例中,我们获取代表新国家选择的新值,然后获取所选国家的合适的资本,最后在资本属性中设置结果以便由页面显示,如清单 4-11 所示。
而不是通过更改 ValueHolder(或 EditableValueHolder)组件的值并单击 CommandButton 或 CommandLink 来触发值更改事件。当 ValueHolder(或 EditableValueHolder)组件的值更改时,可以通过在组件的值更改时提交表单来触发值更改事件。清单 4-13 显示了如何通过移除 CommandButton 并提交值更改表单来应用此行为。
清单 4-13。 通过提交 SelectOneMenu 的值更改表单来执行值更改监听器
<h:form>
<h:outputLabel for="countries" value="Select a country: "/>
<h:selectOneMenu id="countries" value="#{country.name}"
valueChangeListener="#{country.findCapital}"
onchange="submit();">
<f:selectItem itemLabel="---" itemValue="---"/>
<f:selectItem itemLabel="United States" itemValue="USA"/>
<f:selectItem itemLabel="Egypt" itemValue="Egypt"/>
<f:selectItem itemLabel="Denmark" itemValue="Denmark"/>
</h:selectOneMenu> <br/>
<h:outputText value="Capital of #{country.name} is #{country.capital}"
rendered="#{country.capital ne null}"/>
</h:form>
除了使用默认值更改侦听器之外,您还可以编写自己的自定义值更改侦听器。这可以通过创建实现 value change listener 接口的自定义值更改侦听器类来实现。清单 4-14 显示了 CountryValueChangeListener,它利用 ValueChangeListener 并实现 processValueChange(),后者获取新选择的国家,然后查找其首都,最后将结果设置在国家管理的 bean 的 capital 属性中。
***清单 4-14。***CountryValueChangeListener 自定义监听器
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ValueChangeEvent;
import javax.faces.event.ValueChangeListener;
public class CountryValueChangeListener implements ValueChangeListener {
@Override
public void processValueChange(ValueChangeEvent event) throws AbortProcessingException {
FacesContext context = FacesContext.getCurrentInstance();
Country country = context.getApplication().evaluateExpressionGet(context,
"#{country}",
Country.class);
String selectedCountryName = (String) event.getNewValue();
if ("USA".equals(selectedCountryName)) {
country.setCapital("Washington");
} else if ("Egypt".equals(selectedCountryName)) {
country.setCapital("Cairo");
} else if ("Denmark".equals(selectedCountryName)) {
country.setCapital("Copenhagen");
}
}
}
为了将自定义值更改监听器附加到 ValueHolder(或 EditableValueHolder)组件,可以在组件内部使用标记。清单 4-15 显示了在清单 4-13 中提到的资本查找器表单的更新,带有自定义值变更监听器更新。
***清单 4-15。***XHTML 页面中自定义值改变监听器的例子
<h:form>
<h:outputLabel for="countries" value="Select a country: "/>
<h:selectOneMenu id="countries" value="#{country.name}"
onchange="submit();">
<f:selectItem itemLabel="---" itemValue="---"/>
<f:selectItem itemLabel="United States" itemValue="USA"/>
<f:selectItem itemLabel="Egypt" itemValue="Egypt"/>
<f:selectItem itemLabel="Denmark" itemValue="Denmark"/>
<f:valueChangeListener type="com.jsfprohtml5.factorial.model.CountryValueChangeListener"/>
</h:selectOneMenu> <br/>
<h:outputText value="Capital of #{country.name} is #{country.capital}"
rendered="#{country.capital ne null}"/>
</h:form>
如前面加粗的行所示,使用与非常相似;主要是,您需要指定 type 属性,该属性引用侦听器类的完全限定类名。
阶段事件
阶段事件发生在 JSF 请求处理生命周期的每个标准阶段的开始和结束,如图图 4-7 所示。
图 4-7 。阶段事件执行时间
阶段事件由阶段监听器处理。如图 4-4 所示,生命周期实例可以有零个或多个附加的阶段监听器,UIViewRoot 可以有零到两个阶段监听器实例。为了创建一个相位监听器,你需要实现 JSF 的相位监听器接口。清单 4-16 显示了 JSF PhaseListener 接口 ?? 的代码。
清单 4-16。 相位监听器接口
package javax.faces.event;
import java.io.Serializable;
import java.util.EventListener;
public interface PhaseListener extends EventListener, Serializable {
public void afterPhase(PhaseEvent event);
public void beforePhase(PhaseEvent event);
public PhaseId getPhaseId();
}
如清单 4-16 所示,PhaseListener 接口有以下方法:
- getPhaseId():该方法返回请求处理阶段的标识符,在此期间,该侦听器对处理 PhaseEvent 事件感兴趣。合法值是由 PhaseId 类定义的单例实例,包括 PhaseId。ANY_PHASE 表示对所有标准阶段 的通知感兴趣。
- beforePhase():当请求处理生命周期的特定阶段的处理即将开始时,将执行该方法。
- afterPhase():这个方法将在特定阶段的处理刚刚完成时执行。
阶段侦听器对于调试不同 JSF 生命周期阶段的执行非常有用。它还可以用于授权 JSF 应用页面。让我们看一个例子,看看如何使用阶段侦听器来授权 JSF 页面。让我们回到第二章的第一个应用例子。该应用缺少的功能之一是用户可以直接打开欢迎应用,而不必通过登录页面。为了保护应用中的欢迎页面(或者其他页面),我们可以为此创建一个阶段侦听器。我们需要在应用中修改的一件事是设置一个会话标志,表明用户通过在登录页面中输入非空的用户名和密码进行了身份验证。清单 4-17 显示了更新后的用户管理 bean。
清单 4-17。 更新用户管理的 Bean
public class User implements Serializable {
private String name;
private String password;
private Profession profession;
private List<String> favoriteSports;
private Map<String, String> spokenLanguages;
...
public String login() {
FacesContext context = FacesContext.getCurrentInstance();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
// User passes through the login page and clicks the "login" button.
session.setAttribute("isAuthenticated", true);
return "welcome";
}
...
}
如前面的代码清单所示,添加了一个会话属性“isAuthenticated ”,用于在 login()方法中将用户标记为已通过身份验证(本例中接受任何输入的非空用户名和密码)。清单 4-18 显示了授权监听器阶段监听器代码。
清单 4-18。 授权监听器相位监听器
package com.jsfprohtml5.firstapplication.model;
import javax.faces.application.NavigationHandler;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;
public class AuthorizationListener implements PhaseListener {
@Override
public void afterPhase(PhaseEvent event) {
FacesContext context = event.getFacesContext();
String currentPage = context.getViewRoot().getViewId();
boolean isLoginPage = currentPage.endsWith("index.xhtml");
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
Object isAuthenticated = session.getAttribute("isAuthenticated");
if (!isLoginPage && isAuthenticated == null) {
NavigationHandler navigationHandler = context.getApplication().getNavigationHandler();
navigationHandler.handleNavigation(context, null, "index");
}
}
@Override
public void beforePhase(PhaseEvent event) {
//Nothing ...
}
@Override
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
}
为了实现页面授权,我们需要在 RESTORE_VIEW 阶段完成后,在 JSF 生命周期上创建一个阶段侦听器。在 afterPhase() API 中,使用 context.getViewRoot()检索当前页面。getViewId()。当页面不是登录页面(index.xhtml)并且用户未通过身份验证时,则使用 NavigationHandler 将用户转发到登录页面。为了在 JSF 生命周期上安装阶段监听器,您需要在 faces 配置文件、中定义它,如清单 4-19 所示。
清单 4-19。 在 Faces 配置文件中定义阶段监听器
<faces-config ...>
...
<lifecycle>
<phase-listener>
com.jsfprohtml5.firstapplication.model.AuthorizationListener
</phase-listener>
</lifecycle>
</faces-config>
提示如果你想在一个特定的视图上应用一个相位监听器,而不是在所有的页面上应用,你可以通过使用如下的< f:phaseListener >标签来实现:<f:phase listener type = " package。customphasetlistener ">(其中 type 属性表示要创建和注册的完全限定的阶段监听器 Java 类名)。标记在嵌套了该标记的 UIViewRoot 上注册了一个 phaseListener 实例。
系统事件
系统事件 在 JSF 2.0 中引入,以允许 JSF 开发者监听高级生命周期事件并对其做出反应。如本章前面所述,系统事件可以发生在 JSF 应用级别(如应用启动或应用拆除)或 JSF 组件级别。与 Faces 事件不同,系统事件会立即发布,这意味着它们不会排队等待生命周期的后续处理阶段。表 4-2 显示了应用级可能发生的不同类型的系统事件(直接扩展 SystemEvent)。
表 4-2 。应用级别的 JSF 系统事件(扩展 SystemEvent)
|
系统事件
|
描述
|
| — | — |
| PostConstructApplicationEvent
| 应用启动完成后立即发布。 |
| PreDestroyApplicationEvent
| 在应用关闭之前立即发布。 |
| ExceptionQueuedEvent
| 当 JSF 应用中出现意外异常时发布。这可能发生在 JSF 生命周期处理的任何阶段。 |
系统事件也可以发生在组件级 。表 4-3 显示了组件级最常见的系统事件类型。以下所有事件都从 ComponentSystemEvent 扩展而来。
表 4-3 。组件级的 JSF 系统事件(扩展组件系统事件)
|
系统事件
|
描述
|
| — | — |
| PreRenderComponentEvent
| 该事件在组件呈现之前发布。 |
| PostAddToViewEvent
| 这个事件是在组件被添加到 JSF 视图之后发布的。 |
| PreValidateEvent
| 该事件在组件即将被验证之前发布。 |
| PostValidateEvent
| 该事件在组件通过验证后立即发布。 |
| PreDestroyViewMapEvent
| 此事件在视图范围映射即将被销毁之前发布。 |
| PostConstructViewMapEvent
| 此事件在视图范围映射创建后立即发布。 |
| PreRenderViewEvent
| 该事件在视图(UIViewRoot
)即将呈现之前发布。 |
| PostRestoreStateEvent
| 该事件在组件状态恢复后立即发布。 |
为了了解我们如何使用系统事件来支持我们的 JSF 应用,让我们回到我们在第三章中创建的订阅者应用。假设我们想要引入一个新的下拉项目,它将包括职业列表,如图图 4-8 所示。职业列表在应用中是静态的,所以会在应用启动时加载一次,在应用关闭 ?? 之前卸载。
图 4-8 。更新的订户应用屏幕
为了实现这个功能,我们可以使用 PostConstructApplicationEvent 在应用启动后加载静态列表数据,使用 PreDestroyApplicationEvent 在应用关闭前进行清理。
清单 4-20 显示了我们的应用的定制系统事件监听器,它将在应用启动后和关闭前被调用。
清单 4-20。 订阅者自定义系统事件监听器
package com.jsfprohtml5.subscriber.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PostConstructApplicationEvent;
import javax.faces.event.PreDestroyApplicationEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
import javax.faces.model.SelectItem;
public class ListingLoader implements SystemEventListener {
public static final String PROFESSIONS_KEY = "professions";
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
Map<String, Object> applicationMap = FacesContext.getCurrentInstance().
getExternalContext().getApplicationMap();
if (event instanceof PostConstructApplicationEvent) {
//Load the listing data in the startup ...
applicationMap.put(PROFESSIONS_KEY, getSampleProfessionList());
} else if (event instanceof PreDestroyApplicationEvent) {
//Unload the listing data in the shutdown ...
applicationMap.remove(PROFESSIONS_KEY);
}
}
@Override
public boolean isListenerForSource(Object source) {
return source instanceof Application;
}
private List<SelectItem> getSampleProfessionList() {
List<SelectItem> sampleProfessions = new ArrayList<SelectItem>();
sampleProfessions.add(new SelectItem("Profession1"));
sampleProfessions.add(new SelectItem("Profession2"));
sampleProfessions.add(new SelectItem("Profession3"));
sampleProfessions.add(new SelectItem("Other"));
return sampleProfessions;
}
}
为了实现 SystemEventListener 接口,我们需要为两个方法提供实现:
- isListenerForSource(对象源):如果事件侦听器对从源对象接收事件感兴趣,则该方法应该返回 true。对于我们的侦听器来说,事件侦听器只对接收来自应用对象的事件感兴趣。
processEvent(SystemEvent 事件):一旦接收到 SystemEvent 并准备好进行处理,就会调用该方法。在我们的侦听器中,事件对象被检查为 PostConstructApplicationEvent 或 PreDestroyApplicationEvent。如果是 PostConstructApplicationEvent,则在应用映射中键为 PROFESSIONS_KEY 的映射条目中设置专业列表。在 PreDestroyApplicationEvent 中,可以执行任何可能的清理。在我们的侦听器中,职业列表只是从应用地图中移除。
为了允许我们的应用的定制系统事件监听器在 PostConstructApplicationEvent 或 PreDestroyApplicationEvent 中执行,我们需要在 faces 配置文件(faces-config.xml)中注册我们的定制系统事件监听器,如清单 4-21 所示。
清单 4-21。 在 faces-config.xml 中注册我们的定制系统事件监听器
<faces-config ...>
<application>
<system-event-listener>
<system-event-class>javax.faces.event.PostConstructApplicationEvent</system-event-class>
<system-event-listener-class>com.jsfprohtml5.subscriber.model.ListingLoader</system-event-listener-class>
</system-event-listener>
<system-event-listener>
<system-event-class>javax.faces.event.PreDestroyApplicationEvent</system-event-class>
<system-event-listener-class>com.jsfprohtml5.subscriber.model.ListingLoader</system-event-listener-class>
</system-event-listener>
...
</application>
</faces-config>
使用元素中的元素,我们可以在特定的系统事件上注册一个系统事件监听器。它有两个主要元素:元素,表示系统事件类的全限定类名,以及元素,表示系统事件监听器类的全限定类名。
在我们的示例中,我们声明了两个元素,以允许我们的自定义 SystemEvent 侦听器(ListingLoader)接收来自 post constructionapplicationevent 和 PreDestroyApplicationEvent 的事件。
注意<系统-事件-监听器>下还有一个可选元素,就是<源类>元素。< source-class >元素可用于指定事件源的全限定类名。
既然我们已经看到了可以在应用级别发生的系统事件的示例,那么让我们看看如何利用可以在组件级别发生的系统事件。假设我们想要定制订户申请表单的输入字段上的错误显示方式,以便当我们在验证中有一个错误(或一组错误)时,输入字段将被突出显示。这实际上是组件系统事件的一个完美用例。为了实现这个用例,我们需要利用 postValidate 事件。
JSF 引入了标签,我们可以直接将它作为任何 JSF HTML 组件的子组件,以便在其上安装 ComponentSystemEventListener 实例。表 4-4 显示了< f:event >标签的属性。注意< f:event >标签提到的属性是强制的 。
表 4-4 。< f:事件>标签属性
|
属性
|
描述
|
| — | — |
| 类型* | 计算结果为字符串的值表达式,该字符串表示要为其安装侦听器的事件的名称。有效值为preRenderComponent
、preRenderView
、postAddToView
、preValidate
和postValidate
。除了提到的有效值,在用@NamedEvent
注释对扩展类进行注释后,扩展ComponentSystemEvent
的任何 java 类的全限定类名都可以用作“type”属性的值。 |
| 听众* | 方法表达式,必须计算为采用ComponentSystemEvent
作为参数、返回类型为void
的公共方法,或者不采用参数、返回类型为void
的公共方法。 |
为了改变输入组件的风格,我们可以在其中放置一个 和一个< f:event >标签。清单 4-22 显示了如何监听用户名输入字段中的 postValidate 事件。
清单 4-22。 监听用户名输入字段中的 postValidate 事件
<h:inputText id="userName"
value="#{person.name}"
required="true"
requiredMessage="#{bundle['user.name.required']}">
<f:event type="postValidate" listener="#{person.checkName}"/>
<f:validateBean validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup"/>
</h:inputText>
JSF 方法表达式#{person.checkName}检查用户名输入是否有效。如果用户名无效,则在输入字段中添加一个特定的样式类。清单 4-23 显示了 checkName 方法的代码。
清单 4-23。 检查名称方法代码
@ManagedBean
@RequestScoped
public class Person implements Serializable {
...
public void checkName(ComponentSystemEvent componentSystemEvent) {
UIComponent component = componentSystemEvent.getComponent();
if (component instanceof EditableValueHolder) {
EditableValueHolder editableValueHolder = (EditableValueHolder) component;
if (! editableValueHolder.isValid()) {
component.getAttributes().put("styleClass", "invalidInput");
} else {
component.getAttributes().put("styleClass", "");
}
}
}
}
正如我们在清单中看到的,listener 方法将 ComponentSystemEvent 作为参数。使用 getComponent(),我们可以检索组件实例,然后使用 EditableValueHolder 的 is valid()方法检查组件是否有效(通过验证阶段)。如果组件无效,则将 invalidInput CSS 样式类添加到组件的 style class 属性中。invalidInput CSS 样式类很简单,如清单 4-24 中的 所示。
清单 4-24。 invalidInput 风格类
.invalidInput {
background-color: red;
color: white;
}
图 4-9 显示了用户名输入字段没有通过验证时的样子。
图 4-9 。出现错误时设置用户名输入字段的样式
在每个输入组件上安装组件系统事件监听器,在出现错误的情况下使用对这些输入元素进行样式化可能效率不高,并且会使我们在所有的应用表单中有很多重复。因此,在我们的 JSF 应用中,我们需要一种统一而简单的方法来控制所有输入字段中的错误显示。
为了统一控制所有输入字段中的错误显示,我们可以在 PostValidateEvent 上创建一个定制的系统事件侦听器,并通过将 input text class 设置为事件源,将其应用于应用中的所有输入文本元素。清单 4-25 显示了 ErrorDisplayListener 类(我们定制的系统事件监听器类)。
***清单 4-25。***ErrorDisplayListener 类
package com.jsfprohtml5.subscriber.model;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
public class ErrorDisplayListener implements SystemEventListener {
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
UIComponent component = (UIComponent) event.getSource();
if (component instanceof EditableValueHolder) {
EditableValueHolder editableValueHolder = (EditableValueHolder) component;
if (! editableValueHolder.isValid()) {
component.getAttributes().put("styleClass", "invalidInput");
} else {
component.getAttributes().put("styleClass", "");
}
}
}
@Override
public boolean isListenerForSource(Object source) {
return source instanceof UIComponent;
}
}
如前面的清单所示,在 processEvent()中,任何具有验证错误的 EditableValueHolder 组件都将附加 invalidInput 样式。最后,为了将系统事件监听器应用到输入字段,我们需要在 faces 配置(faces-config.xml)中注册系统事件监听器,如清单 4-26 所示。
清单 4-26。 在 Faces 配置文件中注册 ErrorDisplayListener
<faces-config ...>
<application>
...
<system-event-listener>
<source-class>javax.faces.component.html.HtmlInputText</source-class>
<system-event-class>javax.faces.event.PostValidateEvent</system-event-class>
<system-event-listener-class>com.jsfprohtml5.subscriber.model.ErrorDisplayListener
</system-event-listener-class>
</system-event-listener>
</application>
</faces-config>
使用元素强制 postValidateEvent 的源为(javax . faces . component . html . html input text),这是 JSF 输入文本元素类。编写并注册我们的自定义系统事件监听器后,当我们有一个或多个验证错误时,我们将能够自动看到样式化的输入元素,如图图 4-10 所示。
图 4-10 。当我们有一个或多个验证错误时,样式化的输入元素
注更新后的应用(首次应用和订阅应用)可从该书网站
www.apress.com/9781430250104
下载;你可以在第四章的压缩文件中找到完整的源代码。
查看参数
为了支持可加书签的页面,JSF 2.0 中引入了视图参数来支持可寻址页面。视图参数允许 JSF 页面是 RESTful 的,这意味着它们可以被最终用户在浏览器中加入书签,以便他们以后可以随时返回使用这些页面。可以使用< f:viewParam >标签在 JSF Facelets 页面中创建视图参数。清单 4-27 展示了如何在 JSF 页面(car.xhtml)中定义< f:viewParam >标签。
清单 4-27。 在 JSF 页面内使用< f:viewParam >标签(car.xhtml)
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<f:metadata>
<f:viewParam name="model" value="#{car.model}"/>
<f:viewParam name="color" value="#{car.color}"/>
</f:metadata>
<h:head>
<title>Car Information</title>
</h:head>
<h:body>
<p>
Car Model: #{car.model} <br/>
Car Color: #{car.color}
</p>
</h:body>
</html>
在前面的代码中,我们在页面中定义了两个视图参数。每个视图参数都有两个主要属性,name 属性指定请求参数的名称,value 属性表示请求参数的值将绑定到的值表达式。这意味着名称为 model 的请求参数的值将绑定到#{car.model}表达式,名称为 color 的请求参数将绑定到#{car.color}表达式。清单 4-28 显示汽车管理 bean 。
清单 4-28。 车飞龙豆
@ManagedBean
@RequestScoped
public class Car {
private String model;
private String color;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
使用以下参数从浏览器调用 car.xhtml 页面后:
/car.xhtml?model=300D&color=black
这将在 car.xhtml 页面中产生以下内容:
Car Model: 300D
Car Color: black
注意重要的是要知道< f:viewParam >导致一个 uiviewparam 作为当前视图的元数据被附加。UIViewParameter 扩展了 UIInput,这意味着通常对 UIInput 实例采取的任何操作对该类的实例都有效。因此,您可以将转换器、验证器和值更改监听器附加到< f:viewParam >标签上。
如前一篇技巧文章所示,我们可以将转换器和验证器都添加到标签中。让我们看看如何在 car.xhtml 页面中验证视图参数。让我们修改汽车管理 bean,添加一个新属性来描述汽车号码,如清单 4-29 所示。
清单 4-29。 修改汽车管理豆
@ManagedBean
@RequestScoped
public class Car {
//...
private Long number;
//...
public Long getNumber() {
return number;
}
public void setNumber(Long number) {
this.number = number;
}
}
为了强制要求所有的汽车属性都是强制性的,就像我们处理其他 EditableValueHolder 组件一样,我们可以将 required 属性设置为 true。为了验证汽车号码属性在特定的数字范围内是一个有效的数字,我们可以在标签中使用。清单 4-30 展示了我们如何在 car.xhtml 页面中验证视图参数。
清单 4-30。 修改 car.xhtml 页面以利用验证
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<f:metadata>
<f:viewParam name="model" value="#{car.model}"
required="true"
requiredMessage="You need to specify car model"/>
<f:viewParam name="color" value="#{car.color}"
required="true"
requiredMessage="You need to specify car color"/>
<f:viewParam name="number" value="#{car.number}"
required="true"
requiredMessage="You need to specify car number">
<f:validateLongRange minimum="1" maximum="9999999999"/>
</f:viewParam>
</f:metadata>
<h:head>
<title>Car Information</title>
</h:head>
<h:body>
<p>
<h:outputText value="Car Model: #{car.model}" rendered="#{car.model ne null}" /> <br/>
<h:outputText value="Car Color: #{car.color}" rendered="#{car.color ne null}"/> <br/>
<h:outputText value="Car Number: #{car.number}" rendered="#{car.number ne null}"/>
</p>
<h:messages styleClass="errorMessage"/>
</h:body>
</html>
验证错误将显示在中;例如,当 URL 没有指定型号、颜色和数字参数时,或者当数字参数表示无效数字或超出范围的数字时(假定范围是从 1 到 9999999999),就会发生这种情况。
为了支持浏览器书签功能和搜索引擎网络爬虫,JSF 2.0(及更高版本)提供了组件。如果您将组件的 includeViewParams 属性设置为 true,这将生成页面视图参数,作为< h:link >组件生成的 URL 的一部分。例如,如果我们将清单 4-30 中的< h:link >组件添加到 car.xhtml 页面,如下所示:
<h:link includeViewParams="true" value="Can be bookmarked"/>
这将按照以下模式生成一个 URL:
<a href="/contextPath/car.xhtml?model=xxx&color=yyy&number=zzz">Can be bookmarked</a>
注意像< h:link >组件、< h:button >组件都有 includeViewParams 属性;但是,< h:button >为了查看目标页面,会生成一个依赖于 JavaScript onclick 动作的 HTML 按钮,这意味着 web 搜索爬虫无法到达。
除了和之外,还可以在 JSF 动作属性中使用 includeViewParams 参数(作为 JSF 动作结果的一部分),清单 4-31 显示了一个新页面(intro.xhtml)中的 JSF 输入表单,允许用户输入汽车信息,如清单 4-30 中的所示。
清单 4-31。【JSF】用于输入汽车信息的输入表单(intro.xhtml)
<h:form>
<h:panelGrid columns="3">
<h:outputText value="Model:"></h:outputText>
<h:inputText id="model"
value="#{car.model}"
required="true"
requiredMessage="You need to specify car model">
</h:inputText>
<h:message for="model" styleClass="errorMessage"/>
<h:outputText value="Color:"></h:outputText>
<h:inputText id="color"
value="#{car.color}"
required="true"
requiredMessage="You need to specify car color">
</h:inputText>
<h:message for="color" styleClass="errorMessage"/>
<h:outputText value="Car Number:"></h:outputText>
<h:inputText id="number"
value="#{car.number}"
required="true"
requiredMessage="You need to specify car number">
<f:validateLongRange minimum="1" maximum="9999999999"/>
</h:inputText>
<h:message for="number" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="View car details"
action="car?faces-redirect=true&includeViewParams=true" />
</h:form>
如粗体行所示,为了允许 JSF 命令按钮以 RESTful 方式导航到我们的 RESTful 页面(car.xhtml)(目标页面名称和参数将出现在浏览器地址栏中),我们需要向 UICommand 的 action outcome 添加以下两个参数:
- faces-redirect:将 faces-redirect 参数设置为 true 允许当前页面被重定向(而不是转发)到目标页面(我们从第二章中知道)。
- includeViewParams:在操作结果内将 includeViewParams 设置为 true 允许 ActionSource2 组件在执行导航时包含视图参数。请记住,包含的视图参数必须在目标 JSF 页面中声明,在我们的示例中是(car.xhtml)。
假设在 intro.xhtml 页面中输入了有效数据,然后点击“查看汽车详情”按钮,页面将被重定向到(car.xhtml)页面,浏览器地址栏中有以下参数:
contextPath/car.xhtml?model=xxx&color=yyy&number=zzz
这将在 car.xhtml 页面中产生以下内容:
Car Model: xxx
Car Color: yyy
Car Number: zzz
注意知道 JSF 2.x Facelets 是一种基于 XML 的视图技术是很重要的。这意味着(&)字符被解释为 XML 实体的开始。这导致为了表示实际的(&)字符,您必须使用& amp,如最后一个代码示例所示。
摘要
在本章中,您详细学习了 JSF 事件模型。您了解了如何在 JSF 应用中处理行动和价值变更事件。现在,您知道了如何利用 JSF 阶段监听器在您的 JSF 应用中实现一些好东西,比如授权或日志记录。您已经学习了不同类型的 JSF 系统事件,现在知道如何使用应用级和组件级系统事件。最后,您了解了如何使用 JSF 视图参数来使您的 JSF 页面 RESTful。