HTML5 和 JSF 高级教程(三)
五、JSF 2.2:有什么新内容?
JSF 2.2 的工作始于 2011 年初,就在 JSF 2.0 第二次维护发布后的几个月。经过两年的规范和参考实现(Mojarra 2.2)工作,JSF 2.2 于 2013 年 5 月下旬发布。JSF 2.2 也是 2013 年 6 月发布的 Java EE 7 的一部分。JSF 2.2 提供了一些令人兴奋的新特性(也称为大牌特性),许多重大改进,以及相当多的规范说明和错误修复。JSF 2.2 向后兼容早期的 2.0 版本。这是一个好消息,因为您不必重写 JSF 2.0 应用来支持 2.2 的新特性。有几个例外情况,您必须对您的应用进行一些小的更改。
在本章中,我们将介绍和演示 JSF 2.2 的主要特性,并触及一些最重要的变化。最后,我们将看看为了与 JSF 2.2 完全兼容,您需要在 JSF 2.0 应用中实现哪些更改。
关于 JSF 2.2 规范的所有细节,也称为 JSR 344,你可以访问位于 jcp.org/en/jsr/deta… 的 Java 社区进程网站。
大额特征
JSF 2.2 是一个小升级,向后兼容 JSF 2.0。它建立在已经存在的 JSF 功能之上。然而,JSF 2.2 包含了四个新的主要特性。这些是
- HTML5 友好标记:该特性通过允许将任意属性和元素从 Facelet 视图中的 JSF 组件传递到呈现的 HTML 输出,从而为 JSF 添加了 HTML5 支持。
- 资源库契约:这个特性建立在 Facelets 提供的模板化特性之上。使用资源库契约,可以随应用一起提供模板集,或者在应用的类路径中包含的单独 jar 文件中提供模板集。
- Faces Flow:这个特性类似于资源库契约,允许定义和打包用户流以供重用。引入了一个新的 bean 范围来管理流中涉及的 bean 的生命周期。
- 无状态视图:这个特性允许将视图标记为无状态的,因此是暂时的。
JSF 2.2 中还有许多其他较小的变化,我们已经在主要特性之后的章节中介绍了最重要的变化。
HTML5 友好标记
HTML5 肯定是目前最热门的话题之一。这一趋势没有被 JSF 参考小组忽略,他们优先考虑在 JSF 2.2 中实现支持 HTML5 友好标记的特性。
说实话,实现的 HTML5 支持特性也可以用在与 HTML5 不同的上下文中。多年来,web 设计人员一直使用 JavaScript 框架在自定义属性中存储特定于应用的数据。例如,一个网页设计者可能已经选择了包含关于一个图像的附加信息,当这个图像被点击时,这些信息被提取并显示出来(见清单 5-1 )。
清单 5-1。 包含 JavaScript 框架可以提取的数据的自定义属性
<img src="product/1234.png"
title="Click to see more information"
data-popup-title="The product is available for shipping within 24 hours"/>
在 HTML5 之前,自定义属性是非标准的,并且由于浏览器对它们的解释不同,输出通常是不可预测的。HTML5 标准引入了一组新的全局属性和元素,确保浏览器在解释自定义属性时的一致性。大多数新的属性和元素没有 JSF 等价项,因为它们携带的值在服务器端是不相关的。JSF 2.2 通过提供将属性和元素从 Facelet 视图传递到 HTML 代码的能力,引入了对 HTML5 标记的支持。
注意只有用 Facelets 编写的视图才支持属性和元素的传递,用 JSP 编写的视图不支持。
在 JSF 2.2 之前,创建定制属性和元素的唯一方法是将输出包装在一个复合组件中,在该组件中,定制属性和元素以普通的 HTML 格式引入。清单 5-2 和 5-3 显示了包装定制 HTML 属性的复合组件。这种变通办法的问题是它引入了过多的代码,并且输出的代码没有作为一个组件存储在 JSF 组件树中。
清单 5-2。 复合组件包装定制属性(resources/jsf22/img2.xhtml)
<?xml version='1.0' encoding='UTF-8' ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//
EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite">
<cc:interface>
<cc:attribute name="src" type="java.lang.String" required="true" />
<cc:attribute name="title" type="java.lang.String" default="" />
<cc:attribute name="popupTitle" type="java.lang.String" default="" />
</cc:interface>
<cc:implementation>
<img src="#{cc.attrs.src}"
title="#{cc.attrs.title}"s
data-popup-title="#{cc.attrs.popupTitle}" />
</cc:implementation>
</html>
清单 5-3。 Facelets 视图使用 img2 复合组件(img2example.xhtml)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:jsf22="http://java.sun.com/jsf/composite/jsf22">
<h:head>
<title>Example of using img2</title>
</h:head>
<h:body>
<jsf22:img2 src="product/1234.png"
title="Click to see more information"
popupTitle="The product is available for shipping within 24 hours" />
</h:body>
</html>
在接下来的几节中,您将看到 JSF 2.2 如何通过引入传递 HTML 元素和属性的能力,使输出 HTML5 友好的标记变得更加容易。
传递属性
自定义属性可以通过三种不同的方式输出。所有这三种方法的输出是相同的,但是每种方法都有自己最方便的级别,如下所示。
方法 1:使用一次添加一个定制属性
JSF 2.2 引入了一个类似于的新标签,叫做。标签可以嵌套在任何 UIComponent 中,并且有两个属性:name 和 value。Name 包含要添加到父 UIComponent 的自定义属性的名称,value 包含应该存储在具有给定名称的自定义属性中的值。在清单 5-4 中可以看到一个例子。
清单 5-4。 使用< f:PassThroughAttribute / >向 UIComponent 添加两个自定义属性
<h:graphicImage value="product/1234.png"
title="Click to see more information">
<f:passThroughAttribute name="data-popup-title"
value="The product is available for shipping within 24 hours" />
<f:passThroughAttribute name="data-product-id"
value="1234" />
</h:graphicImage>
方法 2:使用添加地图中包含的自定义属性
方法 1 适合于添加一些属性,但是如果您有许多必须添加到组件的属性,并且这些值是在服务器端确定的,那么您可以使用并使用 value 属性提供一个属性映射。清单 5-5 和 5-6 展示了如何从受管 bean 中引用属性映射。
清单 5-5。 使用< f:PassThroughAttributes / >向 UIComponent 添加属性映射
<h:graphicImage value="product/1234.png"
title="Click to see more information">
<f:passThroughAttributes value="#{productDisplay.productAttributes}" />
</h:graphicImage>
清单 5-6。 ViewScoped 托管 Bean 公开了一个产品属性映射,该映射可由< f:PassThroughAttributes / >使用
@ManagedBean
@ViewScoped
public class ProductDisplay {
private Map<String, Object> attributes;
public Map<String, Object> getProductAttributes() {
if (this.attributes == null) {
this.attributes = new HashMap<>();
this.attributes.put("data-popup-title", "Click to see more information");
this.attributes.put("data-product-id", "1234");
this.attributes.put("data-product-name", "Blu-ray Player");
this.attributes.put("data-product-desc", "Complimment your entertainment...");
}
return this.attributes;
}
}
方法 3:使用带前缀的属性直接向 UIComponents 添加自定义属性
作为方法 1 的替代方法,您可以使用带前缀的属性将自定义属性直接添加到 UIComponent 中。属性的前缀是 p,XML 命名空间 URI 是xmlns.jcp.org/jsf/passthrough。这种方法对某些人来说似乎更自然,因为你是直接在 UIComponent 上添加属性,而不包含任何嵌套标签,参见清单 5-7 。
清单 5-7。 使用前缀属性添加自定义属性
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="[`xmlns.jcp.org/jsf/passthrough`](http://xmlns.jcp.org/jsf/passthrough)">
<h:body>
<h:graphicImage value="product/1234.png"
title="Click to see more information"
p:data-popup-title="Available for shipping within 24 hours"
p:data-product-id="1234"/>
</h:body>
</html>
穿过元素
HTML5 引入了 JSF 没有的新元素,比如
和。为了避免页面作者回到编写复合组件的工作中,引入了属性名称空间(jsf)。jsf 名称空间包含通常在 UIComponent 上找到的属性。当使用 jsf 名称空间时,Facelets 会检测到您希望将标记视为 UIComponent,并相应地对其进行映射。在清单 5-8 中,<进度>标签通过使用 jsf:id 属性被转换成 UIComponent。清单 5-8。 使用 jsf 属性命名空间传递元素
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<h:body>
<h:form>
<progress jsf:id="progressbar"
value="#{imageGeneration.progress}"
max="100" />
</h:form>
</h:body>
</html>
从技术上来说,Facelets TagDecorator 负责将定制元素视为 UIComponents。在某些情况下,TagDecorator 会识别出与 HTML 标记完全相同的 UIComponent。在清单 5-9 中,HTML 标签和 JSF 标签将产生相同的输出和组件树。如果你喜欢尽可能接近 HTML 来写你的观点,这是对 JSF 的一个很好的补充。
***清单 5-9。***HTML 和 JSF 标签之间的自动映射
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<h:body>
<h:form>
<input type="text" jsf:value="#{registration.firstName}" />
<h:inputText value="#{registration.lastName}" />
</h:form>
</h:body>
</html>
资源库契约
JSF 2.0 引入了资源库,级联样式表、javascripts、图像和复合组件驻留在 resources/目录中,或者打包在 JAR 文件的 META-INF/resources 目录中。资源库契约通过引入拥有多个资源库的可能性,将这一特性向前推进了一步。使用资源库契约,您可以将模板映射到应用中的特定视图。例如,您可以为匿名用户和经过身份验证的用户或者应用的不同部分使用单独的模板和资源。与普通资源一样,您可以在 contracts/目录下的应用中包含资源库协定,或者通过将协定打包到 META-INF/contracts 目录下的 JAR 文件中。将 JAR 文件放在 WEB-INF/lib 目录中,应用会自动发现它。
提示为了加快资源库契约的发现,在包含契约的目录中放置一个名为 javax.faces.contract.xml 的文件。目前该文件没有任何内容,但在即将推出的版本中可能会有所改变。
有两种使用合同的方法。第一种方法将通过 URL 模式自动将契约映射到视图上。第二种方法在视图中显式声明契约。这些方法可以结合使用,以获得最大的灵活性。首先,我们将看看如何创建资源库契约;然后,我们将看看在应用中应用它们的两种方法。
资源库契约的目标是使一组模板可用,这些模板可由不知道可用资源库契约中正在使用的确切模板的模板客户端重用。因此,资源库契约应该使用相同的模板和内容区域名称。也就是说,模板文件必须具有相同的文件名,并且标记必须使用相同的名称。
例如,我们将在一个应用中创建两个合同。第一个合同在清单 5-10 和清单 5-11 实施,第二个合同在清单 5-12 和清单 5-13 实施。两个模板的区别在于配色方案和帮助文本。
清单 5-10。 文件结构中的"基础"资源库合同应用目录
| contracts/
| contracts/basic/page-template.xhtml
| contracts/basic/layout.css
| contracts/basic/page.css
清单 5-11。 合同目录/基础/页面-模板. xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<h:outputStylesheet name="page.css" />
<h:outputStylesheet name="layout.css" />
<title><ui:insert name="page-title" /></title>
</h:head>
<h:body>
<div id="top" class="top">
<ui:insert name="top" />
</div>
<div id="content" class="center_content">
<ui:insert name="content" />
</div>
</h:body>
</html>
清单 5-12。 文件结构资源库合同后添加一个"基本-附加"合同
| contracts/
| contracts/basic/page-template.xhtml
| contracts/basic/layout.css
| contracts/basic/page.css
| contracts/basic-plus/page-template.xhtml
| contracts/basic-plus/layout.css
| contracts/basic-plus/page.css
| contracts/basic-plus/logo.png
清单 5-13。 合同目录/基础-附加/页面-模板. xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<h:outputStylesheet name="page.css" />
<h:outputStylesheet name="layout.css" />
<title><ui:insert name="page-title" /></title>
</h:head>
<h:body>
<div id="top" class="top">
<h:graphicImage id="logo" name="logo.png" />
<ui:insert name="top" />
</div>
<div id="help" class="left">
Welcome to JSF 2.2\. This example demonstrates how to use Resource Library Contracts.
</div>
<div id="content" class="right ">
<ui:insert name="content" />
</div>
<div id="top" class="top">
You can find more information about JSF 2.2 at the
<a href="http://jcp.org/en/jsr/detail?id=344">JCP website</a>
</div>
</h:body>
</html>
从清单 5-11 中可以看出,资源库契约模板就像普通的 Facelets 模板一样。由于这个模板将是我们合同的基础,我们必须记下并重用模板文件名以及内容区域。也就是说,我们的模板文件必须命名为 page-template.xhtml,并且我们必须坚持使用<ui:insert name = " page-title "/>来插入页面的标题,< ui:insert name="top" / >来插入页面的页眉,< ui:insert name="content" / >来插入页面的内容。您可以自由地更改资源库合同中的所有内容,包括样式表和图像。我们资源库契约的唯一名称是/contracts 下的目录名,也就是 basic。要创建另一个资源库契约,只需在/contracts 下创建一个具有唯一名称的目录,其中包含同名的模板。清单 5-12 显示了在创建了基本加合同之后合同目录的结构。
正如您在清单 5-13 中看到的,模板名称是相同的(page-template.xhtml),内容区域也是相同的(page-title,top,content)。basic 和 basic-plus 遵循相同的约定,可以由相同的模板客户端使用。
资源库契约已经就绪,可以使用了。
方法 1:通过 URL 模式在视图上映射契约
可以通过 URL 模式指定使用哪个契约 。当您希望将不同的资源库应用于不同的部分或访问级别时,这很有用。例如,您可能希望对匿名用户和管理员应用单独的资源库。您可以将资源库契约映射到 applications 标记内 faces-config.xml 中的视图。
在清单 5-14 中,当访问/admin 下的视图时,应用基本的资源库契约。所有其他视图都使用 basic-plus 资源库,其中包含更多帮助信息。
清单 5-14。 对通过/admin/*访问的视图和其余视图应用单独的契约
<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"
FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
<application>
<resource-library-contracts>
<contract-mapping>
<url-pattern>/admin/*</url-pattern>
<contracts>basic</contracts>
</contract-mapping>
<contract-mapping>
<url-pattern>*</url-pattern>
<contracts>basic-plus</contracts>
</contract-mapping>
</resource-library-contracts>
</application>
</faces-config>
提示可以将多个合同映射到一个映射。在这种情况下,它将依次检查每个合同,寻找所需的模板。一旦找到模板,它将停止处理其他合同。
方法二:在每个视图上指定合同
**通过在每个视图上指定契约,您可以让您的应用由用户来设置皮肤。也就是说,您可以允许用户选择为您的应用应用哪个合同。通过将视图包含在一个标签中,您可以将合同应用到模板客户端,在标签中,您可以指定要在合同属性中应用的合同名称,例如。您可以替换视图契约的显式声明,但是使用 EL 绑定,例如,如清单 5-15 和清单 5-16 所示。
清单 5-15。 允许用户选择要应用到视图的合同
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough">
<f:view contracts="#{userSession.contract}">
<ui:composition template="/page-template.xhtml">
<ui:define name="page-title">Welcome to JSF 2.2</ui:define>
<ui:define name="content">
<h:form>
Select a template
<h:selectOneRadio value="#{userSession.contract}" layout="pageDirection" required="true">
<f:selectItem itemValue="basic" itemLabel="Basic" />
<f:selectItem itemValue="basic-plus" itemLabel="Basic Plus" />
</h:selectOneRadio>
<h:commandButton value="Save" />
</h:form>
</ui:define>
<ui:define name="top">Template: #{userSession.contract}</ui:define>
</ui:composition>
</f:view>
</html>
清单 5-16。 会话范围的托管 Bean 用于存储选中的合同
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
@ManagedBean
@SessionScoped
public class UserSession {
private String contract = "basic";
public String getContract() {
return contract;
}
public void setContract(String contract) {
this.contract = contract;
}
}
清单 5-16 可以扩展为从 cookie 或数据库中选择合同,这样用户就不必每次使用应用时都选择要应用的合同。
将合同映射到视图的方法取决于您的应用需求。您可以混合使用这两种方法来获得最大的灵活性。
面流
自从引入 JavaServer Faces 以来,开发人员一直抱怨缺少对覆盖用户流的范围的支持,比如向导和多步注册表单。在 JSF 2.0 中,引入了@ViewScope 注释来支持在同一视图中的可变持久性。@ConversationScoped 是在 JSF 2.0 中为 CDI(组件依赖注入)bean 引入的,通过将 javax . enterprise . context . conversation 接口注入受管 bean,可以开始和结束长时间运行的对话。使用@ViewScope 和@ConversationScoped scopes,您可以实现多步注册表单和向导,但是一旦实现,您会发现最终产品相当分散,不容易重用。Faces flows 通过提供一个完全集成的解决方案来解决这些缺点,在这个解决方案中,您可以在一个受托管 beanss 支持的流定义中指定多个用户流,受托管 bean 用@FlowScoped 进行了注释,并且能够将流打包到单独的目录和 JAR 中。流还可以使用入站和出站参数进行交互。
注意 @FlowScoped 是一个 CDI 作用域,因此您必须在您的应用中启用 CDI,方法是将 beans.xml 包含在 WEB-INF/目录中,或者如果流被打包在 JAR 中,则包含在 META-INF/目录中。
流程定义
您可以在 XML 文件(与其他流文件以-flow.xml 为后缀)中定义流,也可以在用@FlowDefinition 注释的类中定义流。在用@FlowDefinition 注释的类中,您使用 FlowBuilder API 指定流,而 XML 文件使用xmlns.jcp.org/jsf/flowXML 名称空间和模式定义流。使用 FlowBuilder 的好处是,您可以完全编程控制如何定义流。也就是说,您可以基于运行时信息构建您的流。缺点是,不像 XML 版本,仅仅通过查看代码来快速获得流程的概述要困难得多。清单 5-17 显示了用 XML 表达的流程定义。
***清单 5-17。***XML 中的流程定义
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:j="http://java.sun.com/jsf/flow">
<f:metadata>
<j:faces-flow-definition id="newEntryFlow">
<!-- Method to execute when the flow is initialized -->
<j:initializer>#{addressBook.newEntry}</j:initializer>
<!-- Specifies the first node of the flow -->
<j:start-node>newEntryStart</j:start-node>
<!-- Using a switch you can dynamically determine the next node -->
<j:switch id="newEntryStart">
<!-- Go to newEntryHelp if this is the first time the user
is using the wizard -->
<j:navigation-case>
<j:if>#{addressBook.newEntryFirstTime}</j:if>
<j:from-outcome>newEntryHelp</j:from-outcome>
</j:navigation-case>
<!-- Go to basicDetails if this is not the first time that
the user has used the wizard -->
<j:navigation-case>
<j:if>#{!addressBook.newEntryFirstTime}</j:if>
<j:from-outcome>basicDetails</j:from-outcome>
</j:navigation-case>
</j:switch>
<j:view id="newEntryHelp">
<j:vdl-document>newEntryHelp.xhtml</j:vdl-document>
</j:view>
<j:view id="basicDetails">
<j:vdl-document>create-entry-1.xhtml</j:vdl-document>
</j:view>
<j:view id="contactDetails">
<j:vdl-document>create-entry-2.xhtml</j:vdl-document>
</j:view>
<j:view id="contactPhoto">
<j:vdl-document>create-entry-3.xhtml</j:vdl-document>
</j:view>
<!-- The flow can end by navigating to the cancel flow -->
<j:faces-flow-return id="cancel">
<j:navigation-case>
<j:from-outcome>/cancel</j:from-outcome>
</j:navigation-case>
</j:faces-flow-return>
<!-- Method to execute when the flow has ended -->
<j:finalizer>#{addressBook.newEntryFinished}</j:finalizer>
</j:faces-flow-definition>
</f:metadata>
</html>
开始和结束流程
您可以通过在一个动作中调用流的 ID 来启动流。您可以通过返回流程定义中 faces-flow-return 中定义的结果来结束流程。清单 5-18 显示了如何使用命令链接来开始和结束一个流程。
清单 5-18。 您可以通过将流 ID 设置为 UICommand 的动作来启动新的流
<h:commandLink value="Click to add a new entry in the address book" action="newEntryFlow" />
<h:commandLink value="Cancel creating a new entry" action="/cancel" />
遍历流程并存储数据
流量数据有两种存储方式 。您可以将它作为属性存储在用@FlowScoped 注释的 CDI beans 上,如清单 5-19 所示,或者您可以将数据添加到流映射中,该映射保存您放入的任何数据,如清单 5-20 所示。一旦流程结束,地图将被清除。
清单 5-19。 流作用域 Bean 控制逻辑和存储数据
@Named
@FlowScoped(id = "newEntryFlow")
public class AddressBook implements Serializable {
private AddressBookEntry entry;
/**
* Initialiser for the flow.
*/
public void newEntry() {
this.entry = new AddressBookEntry();
...
}
/**
* Determines if this is the first time the new entry flow is being used.
*/
public boolean isNewEntryFirstTime() {
...
}
public AddressBookEntry getEntry() {
return this.entry;
}
}
清单 5-20。 flowScope 可以用来存储任何一种流动期间的物体
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<head>
<title>Enter your name</title>
</head>
<body>
<h:form>
<h:outputLabel for="firstName" value="First name: " />
<h:inputText id="firstName" value="#{flowScope.firstName}" />
<h:outputLabel for="LastName" value="Last name: " />
<h:inputText id="lastName" value="#{flowScope.lastName}" />
<h:commandButton value="Next" action="contactDetails" />
<h:commandButton value="Cancel" action="cancel" />
</h:form>
</body>
</html>
包装
流资源可以驻留在 web 应用中 web 根目录下的目录中,如清单 5-21 所示,也可以打包在一个 JAR 文件中,然后放在/WEB-INF/lib 中,如清单 5-22 所示。页面作者不需要担心流资源如何与应用打包在一起。流资源的引用是相同的,不管它们是如何打包的。
清单 5-21。 驻留在 Web 应用内部的几个流的文件布局
| newEntryFlow/newEntryFlow-flow.xml
| newEntryFlow/newEntryHelp.xhtml
| newEntryFlow/create-entry-1.xhtml
| newEntryFlow/create-entry-2.xhtml
| newEntryFlow/create-entry-3.xhtml
| modifyEntryFlow/modifyEntryFlow-flow.xml
| modifyEntryFlow/modifyEntryHelp.xhtml
| modifyEntryFlow/modify-entry-1.xhtml
| modifyEntryFlow/modify-entry-2.xhtml
| modifyEntryFlow/modify-entry-3.xhtml
清单 5-22。 驻留在 JAR 文件中的流的文件布局
| META-INF/flows/beans.xml
| META-INF/faces-config.xml
| META-INF/flows/newEntryFlow/newEntryFlow-flow.xml
| META-INF/flows/newEntryFlow/newEntryHelp.xhtml
| META-INF/flows/newEntryFlow/create-entry-1.xhtml
| META-INF/flows/newEntryFlow/create-entry-2.xhtml
| META-INF/flows/newEntryFlow/create-entry-3.xhtml
| META-INF/flows/modifyEntryFlow/modifyEntryFlow-flow.xml
| META-INF/flows/modifyEntryFlow/modifyEntryHelp.xhtml
| META-INF/flows/modifyEntryFlow/modify-entry-1.xhtml
| META-INF/flows/modifyEntryFlow/modify-entry-2.xhtml
| META-INF/flows/modifyEntryFlow/modify-entry-3.xhtml
| myflow/NewEntryFlow.class
| myflow/ModifyEntryFlow.class
无状态视图
当请求视图时,JSF 通常会检查状态的副本是否可用(在服务器或客户机上,具体取决于 javax . faces . state _ SAVING _ METHOD 上下文参数的值)。如果请求的视图不存在,则创建该视图,并存储视图中组件的详细信息,供以后检索和处理。在某些情况下,视图可能已经过期,您将会收到可怕的视图过期异常。在高负载应用中,保存和恢复视图所涉及的所有处理都会产生不必要的开销。JSF 2.2 引入了一个简单但强大的特性,叫做无状态视图。使用无状态视图,您可以指定其状态不应被管理的视图。相反,每次请求视图时,视图的状态都被设置为初始状态。通过将上的 transient 属性设置为 true,可以将视图标记为无状态。当使用无状态视图时,应该注意不要依赖任何基于状态的作用域,比如@ViewScope 和@SessionScope。将这些作用域与无状态视图结合使用最终会导致不可预知的行为。启用 JSF 开发模式后,当您组合基于状态的范围和无状态视图时,您将在页面底部看到警告。请注意,无状态视图并不意味着您可以将数据存储在支持 bean 中。事实上,如果您想在使用无状态视图时持久化任何数据,就必须将数据存储在托管 beans 中。无状态视图的一个经典例子是登录页面。登录页面不跟踪状态,只需要在受管 bean 中存储用户名和密码等信息(对于容器管理的安全性,甚至不需要受管 bean 来保存用户输入)。清单 5-23 展示了一个时事通讯注册页面的无状态视图的例子。一旦页面被提交,就没有必要保留视图,因此它是无状态视图的一个很好的候选。
清单 5-23。 无状态查看报名简讯
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<f:view transient="true">
<ui:composition template="/page-template.xhtml">
<ui:define name="page-title">Newsletter Sign-up</ui:define>
<ui:define name="content">
<h:form>
Your e-mail address:
<h:inputText value="#{newsletterSubscription.email}" />
<h:commandButton action="#{newsletterSubscription.subscribe}"
value="Subscribe" />
<h:commandButton action="#{newsletterSubscription.unsubscribe}"
value="Unsubscribe" />
</h:form>
</ui:define>
</ui:composition>
</f:view>
</html>
警告无状态视图是一个全新的特性,并不是所有的组件都经过了彻底的测试,在某些情况下这可能会导致不可预知的行为。在对第三方 JSF 组件库使用无状态视图时,应该特别小心。
其他重大变化
除了大型功能之外,还有许多较小的增强。以下是最重要的小变化的总结。
UIData 支持集合接口,不支持列表
从 UIData 派生的组件现在支持 java.util.Collections 作为内部数据模型。在 JSF 2.2 之前,java.util.List 是唯一受支持的集合。这一变化表明 ORM 通常使用 java.util.Set 集合来映射相关数据。
WAI-ARIA 支持
JSF 2.2 在 HTML 组件上实现了角色属性,以支持 Web 可访问性倡议——可访问的富互联网应用套件(WAI-ARIA)。角色属性用于描述 HTML 标签的用途。更多关于 WAI-ARIA 的信息可以在 www.w3.org/WAI/intro/a… 的找到。清单 5-24 展示了一个如何使用角色属性给面板网格添加含义的例子。
清单 5-24。 表示 panelGrid(表格)是包含选项的菜单
<h:panelGrid role="menu">
<h:commandLink role="menuitem"value="Home" action="/home" />
<h:commandLink role="menuitem"value="Registration" action="/registration" />
...
</h:panelGrid>
JSF 2.2 引入了一个新的视图元数据标签。标记的目的是允许在呈现响应之前进行预处理。预处理可以包括从数据库获取数据或者检查改变导航流的条件。例如,您可以使用 viewAction 从数据库中加载要显示的实体。如果数据库中不存在请求的实体,您可以将用户重定向到一个视图,表明该文件不再存在。清单 5-25 显示了一个 Facelets 文件,它有一个 viewAction,在查看页面时加载一条记录。
清单 5-25。 Facelet 视图使用 f:viewAction 加载记录进行显示
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<f:view>
<f:metadata>
<f:viewParam name="id" value="#{recordDisplay.id}" />
<f:viewAction execute="#{recordDisplay.load}" onPostback="false" />
</f:metadata>
</f:view>
<h:head>
<title>View Record #{recordDisplay.id}</title>
</h:head>
<h:body>
<h1>Record ##{recordDisplay.id}</h1>
<h:panelGrid columns="2">
<h:outputText value="Name:" />
<h:outputText value="#{recordDisplay.record.name}" />
<h:outputText value="Description:" />
<h:outputText value="#{recordDisplay.record.description}" escape="false" />
</h:panelGrid>
</h:body>
</html>
清单 5-26 包含当试图访问一个不存在的记录时应该使用的导航规则。该规则规定,如果返回 false,浏览器应该被重定向到/not-found.xhtml 文件。
***清单 5-26。***faces-config . XML 中的导航案例,如果无法加载实体,该案例会重定向用户
<faces-config>
<navigation-rule>
<navigation-case>
<from-action>#{recordDisplay.load}</from-action>
<from-outcome>false</from-outcome>
<to-view-id>/not-found.xhtml</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
</faces-config>
清单 5-27 是包含导航规则中使用的逻辑的受管 bean。它还是被访问的 Facelets 视图的后台 bean。
清单 5-27。 用于加载记录的动作方法,如果记录被加载则发出信号
@ManagedBean
@RequestScoped
public class RecordDisplay {
@EJB private RecordService recordService;
private Long id;
private Record record;
public Long getId() {
return id;
}
/**
* Used by f:viewParam to set the ID of the record to load.
*
* @param id Unique identifier of the record to load
*/
public void setId(Long id) {
this.id = id;
}
/**
* Loads the record with the ID specified in the viewParam.
*
* @return true if the record was loaded successfully, otherwise false if it wasn’t found
*/
public boolean load() {
try {
record = recordService.findById(this.id);
return true;
} catch (EntityNotFoundException ex) {
return false;
}
}
}
该功能与非常相似,但有几点不同:
- 使用,开发者有责任在先决条件失败的情况下重定向导航。
- 在组件树生成后执行(即在渲染响应阶段),而在组件树生成前执行(即在应用阶段)。
文件上传
最后,在 JSF 几乎十年没有标准文件上传组件之后,JSF 2.2 引入了组件。在 JSF 2.2 之前,开发者必须开发自己的文件上传组件或者使用组件库,比如 RichFaces 或者 PrimeFaces。作为 JSF 2.2 的附加功能,文件上传组件还支持 Ajax 请求中的文件上传。
使用该组件相当简单。在 enctype 设置为“multipart/form-data”的表单中包含标记。将的 value 属性设置为 javax.servlet.http.Part 类型的对象。在提交表单时,用户选择的文件被传输到服务器,并且通过 getInputStream()方法在 Part 对象中可以获得对该文件的引用。您可以使用标签上的 validator 属性为文件上传添加验证。在 validator 中,您可以检查文件大小、内容类型、文件名、文件内容以及请求中随文件一起发送的任何其他头。要在 Ajax 请求中上传文件,只需将标记添加到提交表单的中。
例如,清单 5-28 显示了一个包含输入文件组件的表单。该示例通过在启动上传的 commandButton 中包含 f:ajax 组件来使用 Ajax 上传文件。
清单 5-28。 将照片上传到托管 Bean 的表单
<h:form id="frm-photo-upload"
enctype="multipart/form-data">
<h:outputLabel for="photo" value="Please select your photo and click Upload Photo" />
<h:inputFile id="photo" value="#{myProfile.photo}" validator="#{myProfile.validatePhoto}" />
<h:commandButton value="Upload Photo" action="#{myProfile.uploadPhoto}">
<!-- Remove the f:ajax tag for plain old file upload -->
<f:ajax execute="photo" render="@all" />
</h:commandButton>
<h:messages />
</h:form>
清单 5-29 是上传表单的管理 bean。它包含用于验证上传的文件是一个图像并且大小小于 2 MB 的方法,以及一个上传方法,其中上传的文件的内容是使用来自 Apache Commons IO 项目的 IOUtils 类提取的。 1
清单 5-29。 托管 Bean 接收并处理文件上传
import java.io.IOException;
import javax.ejb.EJB;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.servlet.http.Part;
import org.apache.commons.io.IOUtils;
@ManagedBean
@RequestScoped
public class MyProfile {
@EJB private UserProfileService userProfileService;
private UserProfile userProfile;
private Part photo;
public String uploadPhoto() throws IOException {
// Uploading file. You don't have to do anything here, but you could
// use it for post processing. Don't use this method for validating
// the uploaded file.
byte[] photoContents = IOUtils.toByteArray(photo.getInputStream());
userProfileService.savePhoto(userProfile, photoContents);
FacesContext.getCurrentInstance().addMessage("frm-photo-upload",
new FacesMessage(FacesMessage.SEVERITY_INFO, "Photo uploaded successfully",
"Name: " + photo.getName() + " Size: " + (photo.getSize()/1024) + " KB"));
return "/photo-uploaded";
}
/**
* Validator for checking that the file uploaded is a photo and that the file
* size is less than 2MB.
*/
public void validatePhoto(FacesContext ctx, UIComponent comp, Object value) {
// List of possible validation errors
List<FacesMessage> msgs = new ArrayList<>();
// Retrieve the uploaded file from passed value object
Part photo = (Part)value;
// Ensure that the file is an image
if (!"image/".startsWith(file.getContentType())) {
msgs.add(new FacesMessage("The uploaded file must be an image"));
}
// Ensure that the file is less than 2 MB
if (file.getSize() > 2048) {
msgs.add(new FacesMessage("The uploaded file is larger than 2MB"));
}
// Determine if a validation exception should be thrown
if (!msgs.isEmpty()) {
throw new ValidatorException(msgs);
}
}
public Part getPhoto() {
return photo;
}
public void setPhoto(Part photo) {
this.photo = photo;
}
public UserProfile getUserProfile() {
return this.userProfile;
}
public void setUserProfile(UserProfile userProfile) {
this.userProfile = userProfile;
}
}
Ajax 请求延迟
在 JSF 2.2 中,延迟属性被添加到了标签中。该属性接受一个整数,该整数包含 Ajax 请求执行前等待的毫秒数。当用户使用键盘输入 Ajax 时,这非常有用。它不是在用户输入一个键时立即执行请求,而是在执行请求之前等待几毫秒,看看是否输入了另一个键。如果输入另一个键,前面的 Ajax 请求被取消,只执行最后一个请求。清单 5-30 展示了一个 Ajax 请求在按下一个键后被延迟 1.5 秒的例子,让用户有时间在发送请求前完成输入。
清单 5-30。 将 Ajax 请求延迟 1.5 秒
<h:inputText value="#{registrationBean.username}" >
<f:ajax event="keyup" delay="1500"render="confirmation" />
</h:inputText>
<h:outputText id="confirmation" value="#{registrationBean.confirmationMsg}" />
新的 XML 名称空间
在前面的例子中,您可能已经注意到,JSF 2.2 引入了新的 XML 名称空间。旧的名称空间以 java.sun.com 的开始,而新的名称空间以 http://xmlns.jcp.org 的开始。旧的名称空间现在仍然有效,但是看起来在未来的版本中将会被删除。新的 XML 名称空间在表 5-1 中列出。
表 5-1 。JSF 图书馆的新 XML 名称空间
|
图书馆
|
老 URI
|
新建 URI
|
| --- | --- | --- |
| 复合组件 | http://java.sun.com/jsf/composite | http://xmlns.jcp.org/jsf/composite |
| 面孔核心 | http://java.sun.com/jsf/core | http://xmlns.jcp.org/jsf/core |
| Faces HTML | http://java.sun.com/jsf/html | http://xmlns.jcp.org/jsf/html |
| JSTL 核心 | http://java.sun.com/jsp/jstl/core | http://xmlns.jcp.org/jsp/jstl/core |
| JSTL 函数 | http://java.sun.com/jsp/jstl/functions | http://xmlns.jcp.org/jsp/jstl/functions |
| Facelets 模板 | http://java.sun.com/jsf/facelets | http://xmlns.jcp.org/jsf/facelets |
| 传递属性 | http://java.sun.com/jsf/passthrough | http://xmlns.jcp.org/jsf/passthrough |
| 传递元素 | http://java.sun.com/jsf | http://xmlns.jcp.org/jsf |
新的 XML 名称空间应用于清单 5-31 中的空 Facelets 文件。
清单 5-31。 Facelet 视图使用了新的 XML 名称空间
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough"
xmlns:jsf="http://xmlns.jcp.org/jsf">
</html>
向后兼容性
有两个问题影响了向后兼容性,根源在于 JSF 早期版本中不明确的规范。大多数应用将完全不受这两个问题的影响,但是如果您的应用受到影响,下面将描述这些问题。
第一个问题是由于规范中的一个错误,其中异常以静默方式结束(javax . faces . event . methodexpressionvalueechangelistener . processValueChange()和 javax . faces . event . methodexpressionlistener . process action())。随着 JSF 2.2 规范的澄清,以前被吞咽的异常现在被扔给异常处理程序。任何依赖于被吞咽的异常的应用都必须实现一个安全措施,以避免异常无法处理。
第二个问题是由于基于先前 JSF 规范的意外行为。在 JSF 2.2 中,规范被阐明,因此一些返回类型必须改变。具体来说,它是在访问复合接口内复合组件的属性时返回的 PropertyDescriptors。getValue()和 getValue(java.lang.String)现在将分别返回 javax.el.ValueExpression 和 java.lang.Class。任何直接访问 PropertyDescriptors 的应用都必须考虑已更改的返回类型。
摘要
尽管 JSF 2.2 对 JSF 来说并不是一个重大的改进,但很明显 2.2 版本提供了许多社区多年来一直要求的新功能和变化。值得注意的是四大特色,包括 HTML5 友好标记、资源库契约、faces flows 和无状态视图。在其他重大变化中,我们关注了最终进入 JSF 核心的文件上传组件。文件上传组件不仅支持传统的文件上传,还支持基于 Ajax 的上传。
想要升级他们的 JSF 2.0 和 2.1 应用的开发者几乎可以无缝升级。如“向后兼容性”一节所述,很少有问题会破坏向后兼容性,据估计,受影响的应用也很少。
Apache Commons IO 项目是一个使用 IO 功能的库的集合。该项目可以在以下网址找到:【commons.apache.org/proper/comm… **
六、深入:JSF 定制组件
到目前为止,我们已经从页面作者和应用开发人员的角度看了 Java Server Faces。页面作者关心用户界面的创作,即在 Facelet 视图中构建标记、脚本和样式,在 Facelet 视图中利用 JSF 组件引入动态内容和行为。应用开发人员关心的是编写应用的服务器端行为。这包括构建页面作者的 Facelet 视图直接使用的托管 JSF bean,以及包含业务逻辑和持久性机制(如 JPA 实体)的企业 Java bean(EJB)。
在第六章、第七章和第八章中,我们将关注组件编写人员的职责。组件编写器的作用是创建可重用组件的库,这些组件或者支持特定的应用,或者可以在多个应用之间重用。第七章和 8 展示了复合材料组件的开发。本章的其余部分将关注如何创建非复合组件。第九章介绍了两个流行的带有可重用通用组件的库。这些库可供页面作者和应用开发人员使用,不需要组件编写器。本章介绍了 JSF 组件模型以及如何实现定制的可重用组件。在我们深入 JSF 组件模型之前,我们将首先看看面向组件的软件开发的一些特征。当开发组件时,重点是创建一个具有单一明确职责的软件包。该包应该具有最小的相互依赖性,以确保组件和使用该组件的环境之间的低耦合。有了单一责任和低耦合,就有可能实现全面的单元测试,覆盖组件的许多方面,如果不是所有方面的话。它还提供了一个机会来完整地记录如何使用组件以及它是如何设计的。最后,在开发组件时,组件编写人员必须考虑到组件可能会用在不可预见的场景中。这是应用和组件开发的主要区别之一。在应用开发中,可以完全控制提供给用户的应用和功能。在对象之间存在高耦合性和低内聚性的情况下,您可以轻松地编写应用。当编写一个组件时,范围受到单一职责和与外界良好定义的接口的限制。这允许低耦合和高内聚。
换句话说,当您创建一个组件时,您不能对外部环境做任何假设,它必须有一个定义良好的输入和输出接口,这样外部环境就不必考虑组件是如何实现的,而只需依赖组件所公开的契约。
我们将考虑两种类型的定制组件:用户界面(UI)组件,它们对用户来说是可视化的,以及非用户界面(非 UI)组件,它们实现非功能性或开发人员需求。
了解 JSF 组件架构
开发 JSF 是为了解决 web 应用开发过程中出现的实际问题。JSF 解决的问题之一是对可重用组件的支持,这些组件封装了页面作者的实际实现。JSF 附带了一组标准组件,但是当你着手构建一个简单的 web 应用时,这些组件是远远不够的。在开始开发定制组件之前,您必须了解 JSF 组件架构以及组件作者打算如何使用它。
作为组件编写人员,您可以编写两种类型的定制组件,非复合组件和复合组件。非复合部件是 JSF 第一版的一部分,所有标准部件都是非复合部件。JSF 2 中引入了复合组件,并通过 Facelets 视图声明语言(VDL)简化了组件的创建。非复合组件是用 Java 代码实现的,需要了解 JSF 组件架构。
当您开发 JSF 定制组件时,您必须熟悉几个核心类和文件。它们是 UIComponent、渲染器和标记库描述符(TLD)。
UIComponent 是一个抽象类,所有 JSF 组件都扩展了它。UIComponent 负责组件的数据、状态和行为。在一些简单的情况下,它可能还负责渲染输出,但应该避免使用渲染器的功能。Renderer 也是一个抽象类,自定义组件可以创建它来控制向用户呈现组件的 UI。每个组件(或组件库)都必须有一个 TLD 文件,该文件将 UIComponent 公开为一个标记,并将每个 UIComponent 与适当的呈现器相匹配。TLD 文件打包在独立库的 META-INF/目录中,或者如果它与使用组件的 WEB 应用打包在一起,则打包在 WEB-INF/目录中。TLD 文件的名称通常为 COMPONENT-NAME.taglib.xml,例如 my components . taglib . XML。TLD 文件在 web.xml 文件的上下文参数中引用。三个实体之间的关系见图 6-1 。
图 6-1 。组件开发中的两个核心类以及它们是如何绑定在一起的
您还可以将定制组件与其他定制帮助器耦合,例如验证器、转换器和事件监听器,如第三章和第四章中所述。
创建 JSF 定制组件需要四个步骤。
- 创建组件模型和逻辑
- 创建自定义组件类(从 UIComponent 或其子类型之一派生)
- 如果自定义组件委托呈现,则创建自定义呈现器类
- 创建一个 TLD 文件,该文件将组件和渲染器定义和公开为一个标记
现在让我们更详细地了解这些步骤。
RandomText 自定义组件
我们将通过逐步开发一个组件来演示定制组件的实现,该组件从一个名为 random text 的 web 服务生成随机文本,该 web 服务位于 www.randomtext.me 。该组件的目的是根据用户或页面作者的输入生成一些随机文本。该组件可以简单地用于生成随机文本,或者页面作者可以在开发过程中使用它在页面上插入占位符。
步骤 1—创建组件模型和逻辑
当创建组件时,很容易将组件逻辑与必须实现的类混合起来,以使其在框架上工作。然而,这使得组件很难测试,并且您将框架特性与可以封装和重用的逻辑混在了一起。如果框架突然对如何实现组件有了新的或不同的需求,这也使得升级变得困难。
组件创建的第一步是在了解模型和逻辑如何与 UIComponent 和 Renderer 类交互之前构建模型和逻辑。RandomText 组件的模型相当简单。这是一个名为 RandomTextAPI 的简单类,它有一个调用 RandomText REST 服务并返回从该服务接收的输出的方法。清单 6-1 显示了简化服务的源代码。
清单 6-1。RandomTextAPI.java 实现了一个简化的 API,用于从在线 randomtext.me REST 服务中获取随机文本
package com.apress.projsf2html5.chapter6.components;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
/**
* Simple API for obtaining random text from an online REST service.
*
* @see <a href="http://www.randomtext.me">RandomText</a>
*/
public class RandomTextAPI {
/**
* Enumeration containing the type of text to return.
*/
public enum TextType {
/** Return gibberish text. */
gibberish,
/** Return lorem ipsum text. */
lorem
};
/**
* Enumeration containing the type of formatting to return.
*/
public enum OutputTag {
/** Return the output as paragraphs. */
p,
/** Return the output as items in an unordered list. */
ul
}
/**
* URL to the REST service with five parameters.
*/
private static final String API_URL = "http://www.randomtext.me/api/%s/%s-%d/%d-%d";
/**
* Property in the JSON output that we will extract and return.
*/
private static final String PROPERTY_CONTAINING_OUTPUT = "text_out";
/**
* Google's JSON parser for parsing the result from the service.
*/
private JsonParser jsonParser = new JsonParser();
/**
* Gets a random text from the RandomText.me web service.
*
* @param type Type of random text to return, {@link TextType#gibberish} or
* {@link TextType#lorem}
* @param output Type of output to produce, {@link OutputTag#p} (paragraph
* tags) or {@link OutputTag#ul} (a list)
* @param outputCount Number of outputs to produce (i.e. number of
* paragraphs or list items)
* @param wordCountLower Lowest number of words in a single paragraph or
* list item
* @param wordCountUpper Highest number of words in a single paragraph or
* list item
* @return Random text formatted as {@code type} and {@code output}
* @throws IOException
*/
public String getRandomText(TextType type, OutputTag output, int outputCount, int wordCountLower, int wordCountUpper) throws IOException {
// Generate URL based on method inptu
String url = String.format(API_URL, type, output, outputCount, wordCountLower, wordCountUpper);
// Prepare request to the randomtext.me
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
// Process response by reading the content into a StringBuilder
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuilder apiResult = new StringBuilder();
String line;
while ((line = rd.readLine()) != null) {
apiResult.append(line);
}
// Use the GSON Library to parse the JSON response from randomtext.me
JsonElement jsonElement = jsonParser.parse(apiResult.toString());
JsonObject jsonObject = jsonElement.getAsJsonObject();
return jsonObject.get(PROPERTY_CONTAINING_OUTPUT).getAsString();
}
}
这可以很容易地在下一步中直接复制到 UIComponent 的实现中,但是最终会将业务逻辑与组件表示混合起来,当您需要升级组件时,这很可能会带来维护方面的挑战。为了展示将模型和组件的开发分开的好处,您将在清单 6-2 中找到一个测试用例,如果业务逻辑与 UIComponent 实现混合在一起,这个测试用例将更难实现。
清单 6-2。 针对 RandomTextAPI.java 的简单单元测试
package com.apress.projsf2html5.chapter6.components;
import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import static org.junit.Assert.*;
public class RandomTextAPITest {
int outputCount = 10;
int wordCountLower = 3;
int wordCountUpper = 15;
@Test
public void testListRandomText() throws Exception {
RandomTextAPI.TextType type = RandomTextAPI.TextType.gibberish;
RandomTextAPI.OutputTag output = RandomTextAPI.OutputTag.ul;
RandomTextAPI instance = new RandomTextAPI();
String result = instance.getRandomText(type, output, outputCount, wordCountLower, wordCountUpper);
int paragraphCount = StringUtils.countMatches(result, "<li>");
assertEquals("Incorrect number of items in the list", outputCount, paragraphCount);
}
@Test
public void testGibberishParagraphsRandomText() throws Exception {
RandomTextAPI.TextType type = RandomTextAPI.TextType.gibberish;
RandomTextAPI.OutputTag output = RandomTextAPI.OutputTag.p;
RandomTextAPI instance = new RandomTextAPI();
String result = instance.getRandomText(type, output, outputCount, wordCountLower, wordCountUpper);
int paragraphCount = StringUtils.countMatches(result, "<p>");
assertEquals("Incorrect number of paragraphs", outputCount, paragraphCount);
}
@Test
public void testLoremIpsumParagraphsRandomText() throws Exception {
RandomTextAPI.TextType type = RandomTextAPI.TextType.lorem;
RandomTextAPI.OutputTag output = RandomTextAPI.OutputTag.p;
RandomTextAPI instance = new RandomTextAPI();
String result = instance.getRandomText(type, output, outputCount, wordCountLower, wordCountUpper);
int paragraphCount = StringUtils.countMatches(result, "<p>");
boolean containsLoremIpsum = result.contains("Lorem ipsum");
assertEquals("Incorrect number of paragraphs", outputCount, paragraphCount);
assertTrue("Lorem Ipsum was not found in the result", containsLoremIpsum);
}
}
步骤 2—创建自定义组件
现在我们已经有了自己的逻辑,我们可以继续实现组件了。如前所述,所有组件都必须扩展 UIComponent 或它的一个子类型。UIComponentBase 是 UIComponent 的一个子类型。UIComponentBase 提供了除 getFamily()之外的所有抽象方法的默认实现。当您需要一个在 UIComponentBase 实现中无法满足的完整定制解决方案时,您可以实现 UIComponent。在大多数情况下,您会希望实现 UIComponentBase 或一个标准组件,如用于输入组件的 UIInput 或用于输出组件的 UIOutput。在图 6-2 中,你可以看到 UIComponent 类层次结构的 UML 图。
图 6-2 。UIComponent 层次结构的 UML 图
对于我们的随机文本组件,我们将扩展 UIComponentBase 类。这样我们只剩下一个方法要实现,即 getFamily()。每个组件必须有“组件系列”标识符。标识符用于将组件与渲染器相匹配。组件和渲染器的配对在 TLD 完成,您将在步骤 4 中看到。从 JSF 2.0 开始,您必须用@FacesComponent 注释来注释组件。这为您省去了创建标记处理程序的麻烦,而这在以前的 JSF 版本中是必需的。@FacesComponent 需要一个值,即“组件类型”标识符。这类似于“组件系列”标识符。“组件类型”的目的是允许 JSF 应用 singleton 在运行时基于其类型实例化组件。基于清单 6-3 中的代码,可以通过执行以下代码实例化一个新的 ui component:ui component my comp = context . get application()。create component(RandomTextComponent。COMPONENT _ TYPE);
在组件上,我们还必须为我们希望从页面作者那里公开和收集的属性实现 getters 和 setters。带有描述和默认值的属性可在表 6-1 中找到
表 6-1 。随机文本组件的属性
|
属性
|
描述
|
默认
| | --- | --- | --- | | 文字类型 | 要生成的随机文本类型 | 莫名其妙的话 | | 输出标签 | 要生成的 HTML 类型。p 代表段落,ul 代表无序列表。 | P | | 数数 | 要返回的段落或项目数 | Ten | | minWords | 每个段落或项目中返回的最小字数。 | five | | maxWords | 每个段落或项目中返回的最大字数。 | Ten |
.
最后,我们需要一种方法来获得随机文本。该方法将收集属性并调用 RandomTextAPI。RandomTextComponent 的完整列表可以在列表 6-3 中找到。
***清单 6-3。***randomtextcomponent . Java—我们组件的 UIComponent 实现
package com.apress.projsf2html5.chapter6.components;
import java.io.IOException;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponentBase;
// "RandomText" is the Component Type
@FacesComponent(RandomTextComponent.COMPONENT_TYPE)
public class RandomTextComponent extends UIComponentBase {
/** Component family of {@link RandomTextComponent}. */
public static final String COMPONENT_FAMILY = "RandomText";
/** Component type of {@link RandomTextComponent}. */
public static final String COMPONENT_TYPE = "RandomText";
/** Attribute name constant for textType. */
private static final String ATTR_TEXT_TYPE = "textType";
/** Default value for the textType attribute. */
private static final String ATTR_TEXT_TYPE_DEFAULT = "lorem";
/** Attribute name constant for outputTag. */
private static final String ATTR_OUTPUT_TAG = "outputTag";
/** Default value for the outputTag attribute. */
private static final String ATTR_OUTPUT_TAG_DEFAULT = "p";
/** Attribute name constant for count. */
private static final String ATTR_COUNT = "count";
/** Default value for the count attribute. */
private static final Integer ATTR_COUNT_DEFAULT = 10;
/** Attribute name constant for minWords. */
private static final String ATTR_MIN_WORDS = "minWords";
/** Default value for the minWords attribute. */
private static final Integer ATTR_MIN_WORDS_DEFAULT = 5;
/** Attribute name constant for maxWords. */
private static final String ATTR_MAX_WORDS = "maxWords";
/** Default value for the maxWords attribute. */
private static final Integer ATTR_MAX_WORDS_DEFAULT = 10;
@Override
public String getFamily() {
return RandomTextComponent.COMPONENT_FAMILY;
}
// LOGIC
public String getRandomText() throws IOException {
RandomTextAPI api = new RandomTextAPI();
return api.getRandomText(
RandomTextAPI.TextType.valueOf(getTextType()),
RandomTextAPI.OutputTag.valueOf(getOutputTag()),
getCount(),
getMinWords(),
getMaxWords());
}
// ATTRIBUTES
public String getTextType() {
return (String) getStateHelper().eval(ATTR_TEXT_TYPE, ATTR_TEXT_TYPE_DEFAULT);
}
public void setTextType(String textType) {
getStateHelper().put(ATTR_TEXT_TYPE, textType);
}
public String getOutputTag() {
return (String) getStateHelper().eval(ATTR_OUTPUT_TAG, ATTR_OUTPUT_TAG_DEFAULT);
}
public void setOutputTag(String outputTag) {
getStateHelper().put(ATTR_OUTPUT_TAG, outputTag);
}
public Integer getCount() {
return (Integer) getStateHelper().eval(ATTR_COUNT, ATTR_COUNT_DEFAULT);
}
public void setCount(Integer count) {
getStateHelper().put(ATTR_COUNT, count);
}
public Integer getMinWords() {
return (Integer) getStateHelper().eval(ATTR_MIN_WORDS, ATTR_MIN_WORDS_DEFAULT);
}
public void setMinWords(Integer minWords) {
getStateHelper().put(ATTR_MIN_WORDS, minWords);
}
public Integer getMaxWords() {
return (Integer) getStateHelper().eval(ATTR_MAX_WORDS, ATTR_MAX_WORDS_DEFAULT);
}
public void setMaxWords(Integer maxWords) {
getStateHelper().put(ATTR_MAX_WORDS, maxWords);
}
}
你可能已经注意到清单 6-3 中的 getter 和 setter 并不是封装类成员的典型 getter 和 setter。相反,它们使用 UIComponent 类上公开的 StateHelper。如果属性只是使用类成员来存储它们的值,那么这些值会在每次请求后消失,因为它们不会持久存储在任何地方。所有 UIComponents 都实现 PartialStateHolder 接口,目的是每个 UIComponent 都必须管理自己的状态。所有标准组件都实现 PartialStateHolder,并使用 StateHelper 来保存和检索必要的数据。但是,如果您扩展 UIComponent 而不是标准组件,您必须自己管理组件的状态。考虑到 JSF 实现可能在客户端或服务器端存储组件状态(取决于 javax . faces . state _ SAVING _ METHOD 上下文参数的值),组件编写器可能需要做大量工作来实现状态管理。幸运的是,JSF 的作者意识到了这一点,并为任何实现 UIComponent 的类提供了 StateHelper 类。StateHelper 透明地负责保存和恢复视图间组件的状态。参见图 6-3 中的 StateHolder 类层次和方法。基本上,StateHelper 允许我们将一个对象放入一个具有可序列化名称的映射中。稍后,我们可以使用相同的可序列化名称获取(评估)对象。如果请求的名称不可用,则返回空对象。为了避免检查空值,StateHelper 有一个重载的 eval 方法,您可以在该方法中指定要查找的对象的名称,以及在没有找到所请求的对象时应该返回的值。这对于为属性提供默认值很方便。
图 6-3 。StateHolder 和 StateHelper 类
使用 StateHelper 存储和检索值可以通过使用 put 和 eval 方法来实现,如清单 6-4 所示。
清单 6-4。 使用 StateHelper 存储和检索状态值
public void setTextType(String textType) {
// Store the textType value under the constant ATTR_TEXT_TYPE
getStateHelper().put(ATTR_TEXT_TYPE, textType);
}
public String getTextType() {
// Retrieve the value stored under the constant ATTR_TEXT_TYPE
return (String) getStateHelper().eval(ATTR_TEXT_TYPE);
}
public void setOutputTag(String outputTag) {
// Store the outputTag value under the constant ATTR_OUTPUT_TAG
getStateHelper().put(ATTR_OUTPUT_TAG, outputTag);
}
public String getOutputTag() {
// Retrieve the value stored under the constant ATTR_OUTPUT_TAG
// If the ATTR_OUTPUT_TAG constant could not be found, the value
// in the constant ATTR_OUTPUT_TAG_DEFAULT will be returned instead.
return (String) getStateHelper().eval(ATTR_OUTPUT_TAG, ATTR_OUTPUT_TAG_DEFAULT);
}
步骤 3-创建自定义渲染器类
我们已经实现了从页面作者处获取输入并使用 StateHelper 安全存储值的逻辑和组件。接下来,我们需要实现呈现器,将组件可视化呈现给用户。通过在 UIComponent 上实现 encodeXXX 和 decode 方法,实际上可以在没有渲染器的情况下做到这一点。这对于较小的组件可能很有效,对于我们的例子肯定也是如此,但是这种方法是不可伸缩的。呈现器的目的是将组件逻辑从用户界面的呈现中分离出来。此外,单个组件可以具有多个呈现器,用于在不同的客户端设备上创建不同的呈现。为桌面 web 浏览器呈现的标记可能与为移动 web 浏览器呈现的标记不同。因此,即使你有一个像我们的 RandomText 组件这样的小组件,最好还是把渲染器从 UIComponent 中分离出来。图 6-4 显示了抽象渲染器类,它必须被扩展以实现 RandomText 组件的渲染器。
图 6-4 。抽象渲染器类,必须对其进行扩展才能为自定义组件创建渲染器
在渲染器中覆盖的关键方法在表 6-2 中列出。
表 6-2 。为自定义组件创建渲染器时要重写的主要方法
|
方法
|
描述
|
| --- | --- |
| Decode | 为当前请求解码自定义组件上的任何新状态。当您希望接收来自用户的输入时,重写此方法。 |
| encodeBegin | 将自定义组件的开头呈现给响应流。如果希望对子组件进行编码,并且希望在编码之前向用户输出响应,请重写此方法。 |
| encodeChildren | 呈现自定义组件的子组件。当您想要更改子组件的编码方式时,请重写此方法。默认情况下,子组件使用各自的渲染器递归编码。通常不需要重写此方法,除非您想要阻止子级或类似的编码。 |
| encodeEnd | 将自定义组件的结尾呈现给响应流。这是最常见的重写方法。它是渲染器上调用的最后一个编码方法,通常是生成自定义标记并将其添加到响应流的地方。 |
RandomTextRenderer 将扩展 Renderer,并且像组件一样,我们将使用@FacesRenderer 注释对 Renderer 进行注释。该注释有两个强制属性:componentFamily 和 rendererType。组件系列用于指示渲染器针对哪个组件系列。渲染器类型是一个标识符,用于将渲染器与 TLD 中的组件进行匹配。步骤 4 将说明如何使用渲染类型和组件族匹配渲染和组件。
RandomTextComponent 不需要任何子组件,也不需要用户的任何输入。因此,唯一要重写的方法是 encodeEnd 方法。encodeEnd 方法必须输出一个包含类似于清单 6-5 中的简单标记的响应。
清单 6-5。 示例输出 RandomTextRenderer 的标记
<div id="unique-identifier-of-the-component">
... Random text generated by the component ...
</div>
既然我们知道了标记应该是什么样子,我们就可以实现呈现器了。清单 6-6 展示了 RandomTextRenderer 的实现。
清单 6-6。 实现 RandomTextRenderer.java
package com.apress.projsf2html5.chapter6.components;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.FacesRenderer;
import javax.faces.render.Renderer;
@FacesRenderer(componentFamily = RandomTextComponent.COMPONENT_FAMILY, rendererType = RandomTextRenderer.RENDERER_TYPE)
public class RandomTextRenderer extends Renderer {
/** Renderer type of {@link RandomTextRenderer}. */
public static final String RENDERER_TYPE = "com.apress.projsf2html5.components.RandomTextRenderer";
private static final Logger LOG = Logger.getLogger(RandomTextRenderer.class.getName());
@Override
public void encodeEnd(FacesContext context, UIComponent uicomponent) throws IOException {
ResponseWriter writer = context.getResponseWriter();
RandomTextComponent component = (RandomTextComponent) uicomponent;
try {
writer.startElement("div", component);
writer.writeAttribute("id", component.getClientId(), "id");
try {
writer.write(component.getRandomText());
} catch (IOException randomTextException) {
writer.write(randomTextException.getMessage());
LOG.log(Level.SEVERE, "Could not generate random text", randomTextException);
}
writer.endElement("div");
} catch (IOException ex) {
LOG.log(Level.SEVERE, "Could not generate markup", ex);
}
}
}
使用 FacesContext 上的 ResponseWriter,输出所需的标记相当简单。ResponseWriter 包含启动新元素、向现有元素添加属性和结束现有元素的方法。我们需要提供的只是元素或属性的名称和值,以及它们属于哪个组件。为了创建
元素,我们使用 startElement 方法。当元素尚未关闭时,可以使用 writeAttribute 方法添加属性。若要在元素中添加文本,可以使用 write 或 writeText 方法。writeText 将对字符串中的任何 HTML 进行转义,而 write 将转储给定的内容而不进行转义。最后,可以使用 endElement 方法关闭当前打开的元素。在输出中可以包含任意数量的元素和嵌套元素,但是最好将组件放在一个 div 中,并将 ID 属性设置为组件的 clientId 属性。这使得定位组件以及使用 Ajax 更新组件变得容易。
注意当你从用户那里接收内容时,你应该总是使用 writeText,除非你相信用户不会给内容添加 HTML 和 Javascripts。
步骤 4—创建标签库描述符
TLD 的目的是将组件和渲染器作为标签定义和公开给 JSF 框架和页面作者。
TLD 文件用给定名称空间的所有标签定义了单个名称空间。该命名空间基于标准的 XML 命名空间,以避免当您拥有由不同开发人员提供的多个标记库时出现命名冲突。XML 名称空间是唯一的 URI,由标记库的供应商定义。在我们的例子中,我们选择了名称空间com . a press . projsf 2 html 5/random text。要使用标签库,页面作者必须声明他想要使用名称空间和前缀来调用库。在清单 6-7 中可以看到一个声明使用带有 rt 前缀的标签库的例子。您可以为名称空间选择任何想要的前缀。
清单 6-7。 声明使用标签库
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:rt="http://com.apress.projsf2html5/randomtext">
...
...
</html>
定义了名称空间后,列出了名称空间中的每个标签。必须使用表 6-3 中列出的信息声明标签。
表 6-3 。TLD 中的标签详细信息
|
元素
|
描述
|
| --- | --- |
| <tag> | 包围单个标签的外部元素 |
| <tag-name /> | 标签的名称 |
| <component> | 包含标签中组件详细信息的外部元素 |
| <component-type /> | 标记中包含的组件类型。必须与 UIComponent 实现中设置的组件类型相匹配 |
| <renderer-type /> | 用于呈现标记的呈现器类型。必须与渲染器实现中设置的渲染器类型相匹配 |
| </component> | |
| <attribute> | 包含标签上单个属性详细信息的外部元素。对于应该向 JSF 框架和页面作者公开的所有属性,应该重复这一部分 |
| <name /> | 属性的名称 |
| <type /> | 属性的类型(例如 java.lang.String) |
| <method-signature / > | 如果输入是方法而不是类型,则为方法签名 |
| <description /> | 属性的描述。这将出现在页面作者的代码帮助中 |
| <required / > | 确定属性是否必需的布尔值 |
| <display-name / > | ide 使用的属性的用户友好名称 |
| <icon / > | ide 使用的属性的图形表示 |
| </attribute> | |
| </tag> | |
完整的 TLD 文件可以在清单 6-8 中看到。
清单 6-8。/we b-INF/randomtext . taglib . XML
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib
FontName">http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
version="2.0">
<namespace>http://com.apress.projsf2html5/randomtext</namespace>
<tag>
<tag-name>randomtext</tag-name>
<component>
<component-type>RandomText</component-type>
<renderer-type>com.apress.projsf2html5.components.RandomTextRenderer</renderer-type>
</component>
<attribute>
<name>textType</name>
<type>java.lang.String</type>
<description>Type of random text to generate. Either gibberish or lorem</description>
<required>true</required>
</attribute>
<attribute>
<name>outputTag</name>
<type>java.lang.String</type>
<description>Type of HTML to generate. Either p for paras or ul for list.</description>
</attribute>
<attribute>
<name>count</name>
<type>java.lang.Integer</type>
<description>Number of paragraphs or items to return</description>
<required>true</required>
</attribute>
<attribute>
<name>minWords</name>
<type>java.lang.Integer</type>
<description>Minimum number of words to return in each para or item.</description>
</attribute>
<attribute>
<name>maxWords</name>
<type>java.lang.Integer</type>
<description>Maximum number of words to return in each para or item.</description>
</attribute>
</tag>
</facelet-taglib>
将 randomtext.taglib.xml 文件放在/WEB-INF 目录中。将文件放在此目录中并不会使其自动被 JSF 实现发现。我们必须首先告诉 JSF 实现通过 web.xml 中的 javax.faces.FACELETS_LIBRARIES 上下文参数来查看 taglib 文件;参见清单 6-9 。
清单 6-9。 /WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<context-param>
<param-name>javax.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/randomtext.taglib.xml</param-value>
</context-param>
<welcome-file-list>
<welcome-file>faces/example.xhtml</welcome-file>
</welcome-file-list>
</web-app>
我们现在已经实现了组件模型、定制组件、定制组件呈现器和组件的 TLD。最终组件的类图见图 6-5 。最后,我们必须实现一个简单的应用来测试组件是否按预期工作。
图 6-5 。组成 RandomText 自定义组件的类和文件的 UML 图
使用 RandomText 组件的示例
为了演示 RandomText 组件,我们将创建一个应用,其中组件属性可以通过输入控件来控制;参见图 6-6 。
图 6-6 。用于测试 RandomText 组件的输入控件
用户将能够选择要生成的文本类型(文本类型)、如何格式化输出(输出标签)、应该输出多少生成的文本(计数)以及生成的文本的每个块中的最小和最大字数(最小字数和最大字数)。单击“生成”按钮将调用组件,并根据输入控件中的值生成文本。
我们需要一个托管 bean 来保存输入控件中的值。被管理的 bean 可以在清单 6-10 中看到。
***清单 6-10。***example . Java—会话范围的受管 Bean,用于保存输入控件的值
package com.apress.projsf2html5.chapter6.beans;
import javax.inject.Named;
import javax.enterprise.context.SessionScoped;
import java.io.Serializable;
@Named(value = "example")
@SessionScoped
public class Example implements Serializable {
private String textType = "gibberish";
private String outputTag = "p";
private Integer count = 10;
private Integer minWords = 5;
private Integer maxWords = 10;
public String getTextType() {
return textType;
}
public void setTextType(String textType) {
this.textType = textType;
}
public String getOutputTag() {
return outputTag;
}
public void setOutputTag(String outputTag) {
this.outputTag = outputTag;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getMinWords() {
return minWords;
}
public void setMinWords(Integer minWords) {
this.minWords = minWords;
}
public Integer getMaxWords() {
return maxWords;
}
public void setMaxWords(Integer maxWords) {
this.maxWords = maxWords;
}
}
受管 bean 是一个普通会话范围的 bean,为每个输入控件提供 getters 和 setters,以显示给用户。带有输入控件的 Facelets 视图可以在清单 6-11 中看到。清单还展示了定制组件的用法,首先在 rt XML 前缀下声明它的用法,然后调用组件并设置其 ID。所有其他属性都是从支持 bean 中复制的。当调用“Generate”按钮时,backing bean 上的值将通过 Ajax 刷新。Ajax 组件告诉页面执行表单上所有的模型视图更新,然后通过其客户端 ID (rt1)呈现 RandomText 组件。
清单 6-11。 Facelets 视图演示了 RandomText 组件
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:rt="http://com.apress.projsf2html5/randomtext">
<h:head>
<title>RandomText Component Demo</title>
</h:head>
<h:body>
<h1>Example page for the RandomText component</h1>
<h:form>
<h:panelGrid columns="5">
<h:outputText value="Text Type:" />
<h:outputText value="Output Tag:" />
<h:outputText value="Count:" />
<h:outputText value="Min words:" />
<h:outputText value="Max words:" />
<h:selectOneMenu value="#{example.textType}">
<f:selectItem itemValue="gibberish" itemLabel="Gibberish" />
<f:selectItem itemValue="lorem" itemLabel="Lorem Ipsum" />
</h:selectOneMenu>
<h:selectOneMenu value="#{example.outputTag}">
<f:selectItem itemValue="p" itemLabel="Paragraph" />
<f:selectItem itemValue="ul" itemLabel="Unordered List" />
</h:selectOneMenu>
<h:inputText value="#{example.count}">
<f:convertNumber integerOnly="true" />
</h:inputText>
<h:inputText value="#{example.minWords}">
<f:convertNumber integerOnly="true" />
</h:inputText>
<h:inputText value="#{example.maxWords}">
<f:convertNumber integerOnly="true" />
</h:inputText>
<h:commandButton value="Generate">
<f:ajax render=":rt1" execute="@form" />
</h:commandButton>
</h:panelGrid>
</h:form>
<rt:randomtext id="rt1"
textType="#{example.textType}"
outputTag="#{example.outputTag}"
count="#{example.count}"
minWords="#{example.minWords}"
maxWords="#{example.maxWords}" />
</h:body>
</html>
点击生成按钮时的输出示例见图 6-7 和图 6-8 。
图 6-7 。RandomText 组件已经生成了 10 段单词长度在 8 到 13 之间的乱码
图 6-8 。RandomText 组件已经生成了 16 个单词长度在 2 到 15 之间的 Lorem Ipsum 列表项
包装组件
当您有了一个可重用的组件时,下一步自然是在多个项目中使用它,或者将它分发给其他开发人员供一般使用。把你的类放在包里。您必须重新定位的唯一文件是 randomtext.taglib.xml。将该文件放在包结构的/META-INF 目录中。确保 TLD 文件以. taglib.xml 结尾,这是 JSF 实现在检测外部标记库时正在搜索的内容。
清单 6-12。 文件结构用于封装组件
| src/main/java/com/apress/projsf2html5/chapter6/components/RandomTextAPI.java
| src/main/java/com/apress/projsf2html5/chapter6/components/RandomTextComponent.java
| src/main/java/com/apress/projsf2html5/chapter6/components/RandomTextRenderer.java
| src/main/resources/META-INF/randomtext.taglib.xml
摘要
在本章中,我们探讨了构成 JSF 组件架构的各种类和接口。我们提到了两个关键类,UIComponent 和 renderer,以及 TLD 文件。当把类和文件放在一起时,可以为任何目的产生可重用的定制组件。例如,我们开发了一个定制组件,它与 REST 网站对话,根据几个参数生成随机文本。
七、基本 JSF2 HTML5 组件
HTML5 引入了新的 web 表单元素来迎合常见的输入类型。这些新元素都没有 JSF 元素。在这一章中,我们将研究四种新的 HTML5 输入类型,并将它们实现为复合组件。本章中实现的输入类型是输入颜色、日期选择器、滑块和微调器类型。其他组件留给读者作为练习来实现。新输入类型的完整列表可在表 7-1 中找到。
表 7-1 。HTML5 中的新输入类型
输入颜色自定义组件
在本节中,我们将把 HTML5 颜色输入元素实现为一个复合组件。颜色输入的目的是允许用户使用浏览器内置的原生颜色选择器选择简单的颜色。表 7-2 概述了颜色输入元素可用的属性 。不是所有的浏览器都支持颜色选择器,在本章的最后,我们将实现一个后备,以防浏览器不支持这个特性。
表 7-2 。颜色输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| autocomplete | 布尔代数学体系的 |
| list | 字符串(对数据列表的引用) |
| value | 字符串(具有 8 位红色、绿色和蓝色成分的 sRGB 颜色,例如#ff0000 代表红色) |
使用输入元素有两种主要方式:要么允许用户选择任何颜色,要么将颜色数量限制在预定义的颜色列表中。清单 7-1 展示了两者的一个例子。示例结果见图 7-1 。
清单 7-1。 使用 HTML5 颜色输入的例子
<section>
<label for="all-colors">All colors: </label>
<input id="all-colors" type="color" value="#00ff00" />
</section>
<section>
<label for="limit-colors">Limited colors: </label>
<input id="limit-colors" type="color" value="#00ff00" list="basic-colors" />
</section>
<datalist id="basic-colors">
<option value="#000000" label="Black" />
<option value="#ff0000" label="Red" />
<option value="#00ff00" label="Green" />
<option value="#0000ff" label="Blue" />
</datalist>
图 7-1 。HTML5 颜色输入。所有颜色将显示所有颜色的调色板,受限颜色将显示引用的数据列表中指定的调色板
创建复合组件
基于清单 7-1 中的例子,我们需要为颜色输入元素、数据列表元素和选项元素创建一个复合组件 。我们将从创建不使用数据列表的元素的基本版本开始。
清单 7-2。 基本颜色输入元素的复合组件(resources/projs html 5/Input Color . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" name="#{cc.id}"
jsf:value="#{cc.attrs.value}" type="color" />
</div>
</cc:implementation>
</html>
在接口中,组件公开了一个属性(值),所选的颜色将存储在该属性中。默认情况下,颜色设置为黑色(#000000)。在实现中,组件首先由一个普通的
包装,客户端 DOM ID 作为标识符。默认情况下,包装器什么也不做,但是当页面作者需要使用 CSS 来设计页面样式时,它可能会派上用场。在包装器内部,输出实际的颜色元素。通过使用 jsf 属性名称空间,我们告诉 Facelets 输入元素应该被视为 JSF 组件。jsf:value 属性的值引用接口中的 value 属性,以便可以设置和提取选定的颜色。
使用复合组件
使用复合组件 很容易,如清单 7-3 所示。导入复合组件的名称空间(即/resources 下存储组件的目录)。导入名称空间后,可以在<名称空间:componentFileName / >访问组件。
清单 7-3。 使用颜色输入复合构件
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:projsfhtml5="[`xmlns.jcp.org/jsf/composite/projsfhtml5`](http://xmlns.jcp.org/jsf/composite/projsfhtml5)">
<h:head>
<title>Input Color Custom Component</title>
</h:head>
<h:body>
<h1>Input Color Custom Component</h1>
<h:form id="frm">
<projsfhtml5:inputColor id="ic-favcolor" value="#{componentInputColor.color}" />
<h:commandButton value="Submit" />
<h:outputText id="selected-color" value="Color is: #{componentInputColor.color}" />
</h:form>
</h:body>
</html>
复合组件引用了清单 7-4 中受管 bean 上的一个普通字符串属性,名为 ComponentInputColor#color。点击提交按钮时,选择的颜色被保存在属性中进行处理,如图图 7-2 所示。
清单 7-4。【ComponentInputColor.java】代表存储所选颜色的支持 Bean
package projsfandhtml5.chapter7;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
@ManagedBean
@ViewScoped
public class ComponentInputColor {
private String color = "";
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
图 7-2 。运行中的颜色输入复合组件。用户通过单击颜色控件选择颜色,然后单击提交。所选颜色的代码显示在提交按钮旁边
支持列表
许多新的 HTML5 输入元素支持使用列表。列表的目的是限制给定控件中的用户选择。在本节中,我们将创建一个通用组件,它可以被任何支持 list 属性的输入元素重用。
该实现需要两个复合组件:一个表示元素,另一个表示嵌套的元素。
清单 7-5。 表示嵌套的<选项>标签的复合组件(resources/projsfhtml 5/option . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="" />
<cc:attribute name="label" type="java.lang.String" default="" />
</cc:interface>
<cc:implementation>
<option value="#{cc.attrs.value}" label="#{cc.attrs.label}" />
</cc:implementation>
</html>
option composite 组件在其接口中定义了两个属性:一个属性用于标签,一个属性用于值。label 的目的是提供一个用户友好的值表示,该值是在后台使用的机器友好的值。
清单 7-6。 复合组件,表示颜色输入将为选项引用的外部数据列表(resources/projsfhtml 5/Datalist . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/
xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface>
</cc:interface>
<cc:implementation>
<datalist id="#{cc.id}">
<cc:insertChildren />
</datalist>
</cc:implementation>
</html>
关于 datalist 组件,值得注意的一点是,它使用复合组件的 ID 作为自己的 ID。这意味着输入组件可以使用 datalist 的 ID 引用 datalist,而无需考虑名称空间。另一件要注意的事情是,它将嵌套在复合组件中的所有内容插入到 datalist 元素中。
清单 7-7。 使用 Datalist 和 Option 复合组件。选项嵌套在数据列表中
<projsfhtml5:dataList id="available-colors">
<projsfhtml5:option value="#ff0000" label="Red"/>
<projsfhtml5:option value="#00ff00" label="Green"/>
<projsfhtml5:option value="#0000ff" label="Blue"/>
</projsfhtml5:dataList>
清单 7-8。 HTML 从清单 7-7 中的复合组件输出
<datalist id="available-colors">
<option value="#ff0000" label="Red"></option>
<option value="#00ff00" label="Green"></option>
<option value="#0000ff" label="Blue"></option>
</datalist>
剩下的惟一事情是支持 inputColor 组件中的列表。这是通过在接口中引入一个名为 list 的新属性来实现的,并在 color 元素的实现中引用该属性。
清单 7-9。 在 inputColor 组件中实现对列表的支持(resources/projs html 5/input color . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:clientBehavior name="change"
event="change"
targets="#{cc.id}" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" name="#{cc.id}" jsf:value="#{cc.attrs.value}" type="color"
list="#{cc.attrs.list}"/>
</div>
</cc:implementation>
</html>
Ajax——启用组件
JSF 2 引入了带有< f:ajax/ >标签的原生 Ajax 请求 。您可以在复合组件中使用 Ajax 标记,方法是声明您想要广播的事件以及该事件来自复合组件内部的何处。使用 clientBehavior 标记在复合组件的接口中完成公告。
清单 7-10。 当颜色输入的值改变时,广播一个事件进行 Ajax 处理
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:clientBehavior name="change" targets="#{cc.id}" event="change" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" name="#{cc.id}" jsf:value="#{cc.attrs.value}" type="color"
list="#{cc.attrs.list}"/>
</div>
</cc:implementation>
</html>
clientBehavior 有三个属性:name,包含可以在组件外部监听的事件的名称;目标,包含被监视组件的列表;和 event,JavaScript 事件的名称(没有 on 位,例如 onchange 事件应该是 change)被捕获并转发给监听该类型事件的任何人。
清单 7-11。 监听变更事件,执行并呈现组件和输出文本
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:projsfhtml5="http://xmlns.jcp.org/jsf/composite/projsfhtml5"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title>Input Color Custom Component with DataList and Ajax support</title>
</h:head>
<h:body>
<h1>Input Color Custom Component with DataList and Ajax support</h1>
<h:form id="frm">
<projsfhtml5:inputColor id="ic-favcolor" value="#{componentInputColor.color}"
list="available-colors">
<f:ajax event="change" render=":frm:selected-color" execute="@this " />
</projsfhtml5:inputColor>
<projsfhtml5:dataList id="available-colors">
<projsfhtml5:option value="#ff0000" label="Red"/>
<projsfhtml5:option value="#00ff00" label="Green"/>
<projsfhtml5:option value="#0000ff" label="Blue"/>
</projsfhtml5:dataList>
<h:commandButton value="Submit" />
<h:outputText id="selected-color" value="Color is: #{componentInputColor.color}" />
</h:form>
</h:body>
</html>
使用 inputColor 组件接口中的 clientBehavior,您可以用一个标签来嵌套它,在这里您可以监听广播的事件。
不支持的浏览器的回退
如果您在不支持 HTML5 颜色输入的浏览器中尝试了上一章中的示例,您将在屏幕上看到一个文本字段,如图图 7-3 所示,而不是一个颜色选择器。
图 7-3 。在不支持的浏览器上呈现的颜色输入
支持回退要求能够检测浏览器是否支持给定的特性。如果它不支持自定义组件提供的功能,则应提供替代显示。
在回退示例清单 7-12 中,我们首先检查浏览器是否支持颜色输入。如果不支持颜色输入,我们使用 jscolor 库(在 www.jscolor.com 的可以免费获得)提供一个后备,如图图 7-4 所示。
清单 7-12。 支持回退到 JavaScript 颜色选择器
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:clientBehavior name="change" targets="#{cc.id}" event="change" />
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:outputScript library="js" name="jscolor.js" />
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" jsf:value="#{cc.attrs.value}" type="color"
list="#{cc.attrs.list}"/>
<script type="text/javascript">
function html5_supports_input(type) {
var i = document.createElement("input");
i.setAttribute("type", type);
return i.type === type;
}
if (!html5_supports_input('color')) {
// The color input is not supported on the browser.
// Provide an alternative way of rendering the color picker,
// e.g. jscolor ([`jscolor.com/`](http://jscolor.com/))
var componentId = '${cc.clientId}:${cc.id}'.replace(/:/g, "\\:");
new jscolor.color(document.getElementById('${cc.clientId}:${cc.id}'), {})
}
</script>
</div>
</cc:implementation>
</html>
图 7-4 。inputColor 组件的回退版本 ??
提示您可以使用像 Modernizr(www.modernizr.com)这样的 JavaScript 库来检测 HTML5 和 CSS3 的本地实现的可用性,而不是实现您自己的 HTML5 特性检测算法。
输入日期选择器自定义组件
在本节中,我们将把 HTML5 日期选择器输入元素实现为一个复合组件。日期选择器输入的目的是允许用户使用浏览器内置的本地日期选择器选择日期。不是所有的浏览器都支持日期选择器,所以像颜色选择器一样,我们将提供一个后备,这次使用 JQuery-UI。像颜色输入组件一样,日期输入组件也支持 change 事件,我们将捕捉并广播到支持 Ajax 的组件。
表 7-3 包含日期输入元素支持的属性列表。我们将在复合组件中实现所有属性。如何使用属性的例子可以在清单 7-13 中找到。
表 7-3 。日期输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| Autocomplete | 布尔代数学体系的 |
| List | 字符串(对数据列表的引用) |
| Value | 日期字符串(年-月数-日数,例如 1970-01-10 是 1970 年 1 月 10 日日 |
| Max | 日期字符串(用户可以选择的最晚可能日期) |
| Min | 日期字符串(用户可以选择的最早日期) |
| Readonly | 布尔代数学体系的 |
| Required | 布尔代数学体系的 |
| Step | 整数(每步改变的天数) |
清单 7-13。 利用 HTML5 输入日期的例子
<section>
<label for="without-value">Date (without a preset date): </label>
<input id="withouth-value" type="date" value="" />
</section>
<section>
<label for="with-value">Date (with a preset date): </label>
<input id="with-value" type="date" value="2013-05-03" step="10" />
</section>
<section>
<label for="with-constraints">Date (with a constraints): </label>
<input id="with-constraints" type="date" value="2013-05-03"
min="2013-05-01" max="2013-05-31" />
</section>
<section>
<label for="readonly">Date (readonly): </label>
<input id="readonly" type="date" value="2013-05-03"
readonly />
</section>
<section>
<label for="with-list">Date (with list of dates): </label>
<input id="with-list" type="date" value="" list="available-dates" />
</section>
<datalist id="available-dates">
<option value="2013-01-01" label="1st Option" />
<option value="2013-03-10" label="2nd Option" />
<option value="2013-06-19" label="3rd Option" />
<option value="2013-10-10" label="4th Option" />
</datalist>
</section>
创建复合组件
基于清单 7-13 中的 HTML5 示例,我们可以重用颜色输入组件中的 datalist 和 option 组件的实现。请注意,HTML5 中输入的日期值是一个年-月-日格式的字符串(例如 2013-04-20)。为了避免不必要的数据转换,我们将使用日期/时间转换器,以便组件的值可以设置为 java.util.Date 。
清单 7-14 显示了一个支持 HTML5 中指定属性的基本实现。组件的值是一个 java.util.Date,并使用 input 元素中嵌套的< f:convertDateTime / >转换器自动转换。required 和 readonly 属性具有 jsf 等效项,因此通过在属性前添加 JSF 前缀(即 JSF:readonly = " # { cc . attrs . readonly } ")将它们直接映射到 JSF。您可能已经注意到,min 和 max 属性并没有从接口直接映射到实现。不可能将转换器应用于属性值。这意味着我们不能简单地将接口中 min 属性的 java.util.Date 值赋给实现中的 min 属性。如果我们直接映射属性,则实现中 min 属性的值将呈现为 java.util.Date 值的“toString()”表示,其形式为:星期几+月名+月日+小时:分钟:秒+时区+年,例如,星期六 Jun 15 11:24:21 CET 2013。此格式与 HTML5 指定的格式(即年-月-日)不一致。因为我们不能在属性值上应用转换器,所以我们必须为负责数据对话的组件实现一个后备 bean。
清单 7-14。 复合组件为 inputDate 组件(resources/projs html 5/input date . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface>
<cc:attribute name="value" type="java.util.Date" required="true" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.util.Date" />
<cc:attribute name="max" type="java.util.Date" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="date" event="change" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="date"
type="date"
jsf:value="#{cc.attrs.value}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"
step="#{cc.attrs.step}"
list="#{cc.attrs.list}">
<f:convertDateTime pattern="yyyy-MM-dd" />
</input>
</div>
</cc:implementation>
</html>
为复合组件创建支持 Bean
复合组件支持 bean 必须扩展 javax . faces . component . uinamingcontainer。按照惯例,通过将支持 bean 命名为与复合组件相同的名称,并将支持 bean 放在与复合组件所在目录同名的包中,可以自动将支持 bean 映射到复合组件。或者,可以用@FacesComponent 注释来注释支持 bean,并将组件类型指定为注释的值。然后可以在复合组件的接口中指定组件类型。手动映射支持 bean 的好处是,其他复合组件可以重用支持 bean,并且不必将支持 bean 命名为与复合组件相同的名称。
清单 7-15 显示了一个为 inputDate 类型的复合组件声明的后备 bean 的基本示例。通过将接口的 componentType 属性设置为@FacesComponent 注释中声明的名称,复合组件映射到支持 bean,例如<cc:interface component type = " input date ">。
清单 7-15。 为 inputDate 组件声明的空支持 Bean
package projsfhtml5;
import javax.faces.component.FacesComponent;
import javax.faces.component.UINamingContainer;
@FacesComponent("inputDate")
public class InputDateComponent extends UINamingContainer {
}
为了支持 min 和 max 属性,我们需要 backing bean 上的两个属性:一个用于保存最小日期的字符串表示,另一个用于保存最大日期的字符串表示。我们将属性命名为 minDate 和 maxDate。通过为这两个属性提供 getters,它们将自动对复合组件可用。然后,我们将覆盖 encodeBegin 方法,从接口中提取 java.util.Dates,并将它们转换为可以在实现中使用的 HTML5 格式的日期。
清单 7-16。 Backing Bean 转换并暴露最小和最大日期
package projsfhtml5;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import javax.faces.component.FacesComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
@FacesComponent("inputDate")
public class InputDateComponent extends UINamingContainer {
private static final DateFormat HTML5_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private String minDate = "";
private String maxDate = "";
@Override
public void encodeBegin(FacesContext context) throws IOException {
// Extract the minimum date from the interface
java.util.Date attrsMin = (java.util.Date) getAttributes().get("min");
if (attrsMin != null) {
// Convert the date to an HTML5 date
minDate = HTML5_FORMAT.format(attrsMin);
}
// Extract the maximum date from the interface
java.util.Date attrsMax = (java.util.Date) getAttributes().get("max");
if (attrsMax != null) {
// Convert the date to an HTML5 date
maxDate = HTML5_FORMAT.format(attrsMax);
}
super.encodeBegin(context);
}
/**
* Gets the minimum date selectable in the date picker.
*
* @return Date formatted using the {@link inputDate#HTML5_FORMAT}
*/
public String getMinDate() {
return minDate;
}
/**
* Gets the maximum date selectable in the date picker.
*
* @return Date formatted using the {@link inputDate#HTML5_FORMAT}
*/
public String getMaxDate() {
return maxDate;
}
}
使用复合组件
有了后备 bean,我们就可以访问包含转换后的日期的 minDate 和 maxDate 属性,并将它们插入到复合组件的实现中。
清单 7-17。 复合组件使用 Backing Bean 并访问转换后的值
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface componentType="inputDate">
<cc:attribute name="value" type="java.util.Date" required="true" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.util.Date" />
<cc:attribute name="max" type="java.util.Date" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="date" event="change" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="date"
type="date"
jsf:value="#{cc.attrs.value}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"
step="#{cc.attrs.step}"
min="#{cc.minDate}"
max="#{cc.maxDate}"
list="#{cc.attrs.list}">
<f:convertDateTime pattern="yyyy-MM-dd" />
</input>
</div>
</cc:implementation>
</html>
有了复合组件和后台 bean,我们就可以使用该组件了。清单 7-18 显示了一些示例,并附有图 7-5 中的截图。
清单 7-18。 使用 inputDate 复合组件的例子
<h:form>
<h1>JSF Component</h1>
<section>
<label>Date (with a null java.util.Date): </label>
<projsfhtml5:inputDate value="#{componentInputDate.emptyDate}" />
<h:outputLabel value="Selected date:" />
<h:outputText value="#{componentInputDate.emptyDate}">
<f:convertDateTime dateStyle="short" />
</h:outputText>
</section>
<section>
<label>Date (with a java.util.Date): </label>
<projsfhtml5:inputDate value="#{componentInputDate.selectedDate}"
step="10"
min="#{componentInputDate.minDate}"
max="#{componentInputDate.maxDate}" />
<h:outputLabel value="Selected date:" />
<h:outputText value="#{componentInputDate.selectedDate}">
<f:convertDateTime dateStyle="short" />
</h:outputText>
</section>
<section>
<label>Date (readonly): </label>
<projsfhtml5:inputDate value="#{componentInputDate.selectedDate}"
readonly="true" />
</section>
<section>
<label>Date (with a list of dates): </label>
<projsfhtml5:inputDate value="#{componentInputDate.selectedDate2}"
list="available-dates" />
<projsfhtml5:dataList id="available-dates">
<projsfhtml5:option label="1st Option" value="2012-01-01" />
<projsfhtml5:option label="2nd Option" value="2012-01-02" />
<projsfhtml5:option label="3rd Option" value="2012-01-03" />
</projsfhtml5:dataList>
<h:outputLabel value="Selected date:" />
<h:outputText value="#{componentInputDate.selectedDate2}">
<f:convertDateTime dateStyle="short" />
</h:outputText>
</section>
<h:commandButton value="Submit" />
</h:form>
图 7-5 。运行中的<项目 html5:输入日期/ >组件
不支持的浏览器的回退
不支持 HTML 日期输入的浏览器会呈现一个带有日期的纯文本字段,就像上一节中的颜色输入一样。为了提供后备,我们可以再次使用 JavaScript 来检查浏览器是否支持该特性,如果不支持,我们可以提供控件的替代显示。在下面的例子中,我们使用 Modernizr 检查浏览器是否支持该特性,如果不支持,我们使用 JQuery-UI 的 DatePicker 组件,如图 7-6 所示。
图 7-6 。如果浏览器不支持 HTML5 日期输入,则返回 JQuery-UI
清单 7-19。 使用 JQuery-UI 和 Modernizr 支持回退
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface componentType="inputDate">
<cc:attribute name="value" type="java.util.Date" required="true" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.util.Date" />
<cc:attribute name="max" type="java.util.Date" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="date" event="change" />
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="css" name="jquery-ui.css" />
<h:outputScript target="head" library="js" name="modernizr.js" />
<h:outputScript target="head" library="js" name="jquery-1.9.1.js" />
<h:outputScript target="head" library="js" name="jquery-ui.js" />
<script type="text/javascript">
if (!Modernizr.inputtypes.date) {
jQuery(function() {
var id = '${cc.clientId}:date'.replace(/:/g, "\\:");
jQuery("#" + id).datepicker({ dateFormat: 'yy-mm-dd' });
});
}
</script>
<div id="#{cc.clientId}">
<input jsf:id="date" type="date" jsf:value="#{cc.attrs.value}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"
step="#{cc.attrs.step}" min="#{cc.minDate}" max="#{cc.maxDate}"
list="#{cc.attrs.list}">
<f:convertDateTime pattern="yyyy-MM-dd" />
</input></div>
</cc:implementation>
</html>
注意在复合组件中使用< h:outputScript/ >和< h:outputStylesheet/ >的一个好处是,即使在一个页面中多次使用该组件,它也只会被包含一次。
滑块自定义组件
在本节中,我们将把 HTML5 range input 元素实现为一个复合组件。范围输入的目的是允许用户使用浏览器内置的本地滑块控件在数字范围内选择一个值。不是所有的浏览器都支持范围输入,所以像日期选择器一样,我们将使用 JQuery-UI 提供一个后备。我们还将支持 Ajax 事件,因为它们对于滑块向用户提供关于所选值的即时反馈特别有用。
表 7-4 包含范围输入元素支持的属性列表。我们将在复合组件中实现所有属性。如何使用属性 的例子可以在清单 7-20 中找到
表 7-4 。范围输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| autocomplete | 布尔代数学体系的 |
| list | 字符串(对数据列表的引用) |
| value | 代表范围中所选值的数字 |
| max | 范围控件中的上限 |
| min | 范围控件的下限 |
| step | 使用范围控制时要步进的数字 |
清单 7-20。 使用 HTML5 范围输入的例子
<section>
<label for="without-value">Range (without value): </label>
<input id="withouth-value" type="range" />
</section>
<section>
<label for="min-to-max">Range (-10 to 10): </label>
<input id="min-to-max" type="range" min="-10" max="10" value="0" />
</section>
<section>
<label for="range-list">Range (list): </label>
<input id="range-list" type="range" value="0" list="list" />
</section>
<datalist id="list">
<option value="-100" />
<option value="-75" />
<option value="-50" />
<option value="-25" />
<option value="0" />
</datalist>
创建复合组件
范围组件的实现非常简单。相关的属性被添加到接口中,同时广播 JavaScript change 事件。该实现包含 JavaScript 库,用于在浏览器不支持 HTML5 输入范围控件的情况下进行后备。它还包括内联 JavaScript 检查输入范围控件是否受支持,如果不受支持,则使用 JQuery 和 JQuery UI 将
元素转换为范围控件。这个组件中的内联 JavaScript 比前面的组件稍微复杂一些。这是因为 JQuery UI 中的 slider 控件不是应用于一个元素,而是应用于一个元素。这意味着每次移动滑块时,我们都需要更新元素并触发一个 change 事件,以保持功能与原生 HTML5 版本一致。
清单 7-21。 用于 inputRange 的复合组件(resources/projs html 5/input range . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.lang.String" />
<cc:attribute name="max" type="java.lang.String" />
<cc:clientBehavior name="change" targets="range" event="change" />
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="css" name="jquery-ui.css" />
<h:outputScript target="head" library="js" name="modernizr.js" />
<h:outputScript target="head" library="js" name="jquery-1.9.1.js" />
<h:outputScript target="head" library="js" name="jquery-ui.js" />
<script type="text/javascript">
if (!Modernizr.inputtypes.range) {
jQuery(function() {
var rangeId = '${cc.clientId}:range'.replace(/:/g, "\\:");
var hideId = '${cc.clientId}'.replace(/:/g, "\\:");
jQuery("#" + hideId).hide();
var id = '${cc.clientId}:fallback'.replace(/:/g, "\\:");
jQuery("#" + id).slider({
min: #{cc.attrs.min},
max: #{cc.attrs.max},
step: #{cc.attrs.step},
slide: function(event, ui) {
// Update the value of the input element and fire a change event
jQuery("#" + rangeId).val(ui.value).change();
}});
});
}
</script>
<!-- Div used for fallback in case the HTML5 range control is not supported -->
<div id="#{cc.clientId}:fallback" style="display: block"></div>
<div id="#{cc.clientId}">
<input jsf:id="range" type="range" jsf:value="#{cc.attrs.value}"
step="#{cc.attrs.step}" min="#{cc.attrs.min}" max="#{cc.attrs.max}"
list="#{cc.attrs.list}" />
</div>
</cc:implementation>
</html>
使用复合组件
清单 7-22 展示了一些复合控件的例子,并附带了图 7-7 (原生 HTML5 支持)和图 7-8 (回退)中的截图。
清单 7-22。 使用 inputRange 复合组件的例子
<h1>JSF Composite Control</h1>
<section>
<label for="min-to-max">Range (-10 to 10): </label>
<projsfhtml5:inputRange value="#{componentInputRange.range1}" min="-10" max="10">
<f:ajax event="change" execute="@this" render=":frm:selectedValue" />
</projsfhtml5:inputRange>
<h:outputText id="selectedValue" value="Selected value: #{componentInputRange.range1}"/>
</section>
<section>
<label for="with-list">Range (with List): </label>
<projsfhtml5:inputRange value="#{componentInputRange.range2}" min="-100" max="10" list="range-options">
<f:ajax event="change" execute="@this" render=":frm:selectedValue2" />
</projsfhtml5:inputRange>
<h:outputText id="selectedValue2" value="Selected value: #{componentInputRange.range2}"/>
<projsfhtml5:dataList id="range-options">
<projsfhtml5:option value="-100" />
<projsfhtml5:option value="-75" />
<projsfhtml5:option value="-50" />
<projsfhtml5:option value="-25" />
<projsfhtml5:option value="0" />
<projsfhtml5:option value="10" />
</projsfhtml5:dataList>
</section>
<h:commandButton value="Submit" />
图 7-7 。原生 HTML5 支持
图 7-8 。回退到 JQueryUI 滑块
微调器自定义组件
最后,我们将把 HTML5 数字输入元素实现为一个复合组件。数字输入的目的是允许用户使用微调控件在数字范围内选择一个值,用户可以单击向上或向下,之后所选的值递增或递减。像所有其他控件一样,所有浏览器还不支持数字控件,所以我们将再次使用 JQuery-UI 提供一个后备。
表 7-5 包含数字输入元素支持的属性列表。我们将在复合组件中实现所有属性。如何使用属性 的例子可以在清单 7-23 中看到。
表 7-5 。数字输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| Autocomplete | 布尔代数学体系的 |
| List | 字符串(对数据列表的引用) |
| value | 代表所选值的数字 |
| max | 微调控制项中的上限 |
| min | 微调控制项的下限 |
| step | 使用微调控件时要步进的数字 |
| readonly | 布尔代数学体系的 |
| required | 布尔代数学体系的 |
清单 7-23。 利用 HTML5 输入数字的例子
<section>
<label for="without-value">Number (without value): </label>
<input id="withouth-value" type="number" />
</section>
<section>
<label for="min-to-max">Number (-10 to 10): </label>
<input id="min-to-max" type="number" min="-10" max="10" value="0" />
</section>
<section>
<label for="preselect-list">Range (list): </label>
<input id="preselect-list" type="number" value="0" list="list" />
</section>
<datalist id="list">
<option value="-100" />
<option value="-75" />
<option value="-50" />
<option value="-25" />
<option value="0" />
</datalist>
创建复合组件
数字组件的实现类似于范围组件。相关的属性被添加到接口中,同时广播 JavaScript change 事件。该实现包含 JavaScript 库,用于在浏览器不支持 HTML5 输入数字控件的情况下进行后备。它还包括内联 JavaScript 检查是否支持输入数字控件,如果不支持,则使用 JQuery 和 JQuery UI 将输入字段转换为微调控件。每当 spinner 控件用于确保 Ajax 调用被广播时,还会挂接一个事件来触发一个 change 事件。
清单 7-24。 复合组件为 input number(resources/projs html 5/input number . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/
xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.lang.String" />
<cc:attribute name="max" type="java.lang.String" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="number" event="change" />
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="css" name="jquery-ui.css" />
<h:outputScript target="head" library="js" name="modernizr.js" />
<h:outputScript target="head" library="js" name="jquery-1.9.1.js" />
<h:outputScript target="head" library="js" name="jquery-ui.js" />
<script type="text/javascript">
if (!Modernizr.inputtypes.number) {
jQuery(function() {
var id = '${cc.clientId}:number'.replace(/:/g, "\\:");
jQuery("#" + id).spinner();
jQuery('.ui-spinner-button').click(function() {
jQuery(this).siblings('input').change();
});
});
}
</script>
<div id="#{cc.clientId}">
<input jsf:id="number" type="number" jsf:value="#{cc.attrs.value}"
step="#{cc.attrs.step}" min="#{cc.attrs.min}" max="#{cc.attrs.max}"
list="#{cc.attrs.list}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"/>
</div>
</cc:implementation>
</html>
使用复合组件
清单 7-25 展示了复合控件 的一些例子,并附有截图图 7-9 (原生 HTML5 支持)和图 7-10 (回退)。
清单 7-25。 使用 inputNumber 复合组件的例子
<section>
<label for="min-to-max">Number (-10 to 10): </label>
<projsfhtml5:inputNumber value="#{componentInputNumber.number1}" min="-10" max="10">
<f:ajax event="change" execute="@this" render=":frm:selectedValue" />
</projsfhtml5:inputNumber>
<h:outputText id="selectedValue" value="Selected value: #{componentInputNumber.number1}"/>
</section>
<section>
<label for="with-list">Number (with List): </label>
<projsfhtml5:inputNumber value="#{componentInputNumber.number2}" min="-100" max="10"
list="number-options">
<f:ajax event="change" execute="@this" render=":frm:selectedValue2" />
</projsfhtml5:inputNumber>
<h:outputText id="selectedValue2" value="Selected value: #{componentInputNumber.number2}"/>
<projsfhtml5:dataList id="number-options">
<projsfhtml5:option value="-100" />
<projsfhtml5:option value="-75" />
<projsfhtml5:option value="-50" />
<projsfhtml5:option value="-25" />
<projsfhtml5:option value="0" />
<projsfhtml5:option value="10" />
</projsfhtml5:dataList>
</section>
<h:commandButton value="Submit" />
图 7-9 。原生 HTML5 支持
图 7-10 。回退到 JQueryUI 滑块
摘要
在这一章中,我们已经看到了将一些新的 HTML5 输入类型转换成 JSF 复合组件。在这一章中,我们研究了基本复合组件、嵌套组件和支持 Ajax 的组件的实现。复合组件为页面作者隐藏了不必要的逻辑。我们研究了如何检测浏览器是否支持我们输出的输入元素,如果不支持,就提供另一个视图。如果没有复合组件,您可能需要在每个页面上编写额外的 JavaScript 来检查兼容性。使用复合组件,所有的逻辑都包含在一个地方,很容易使用复合组件进行一个简单的更改,然后级联到所有的页面。