|
在最近的一期 文章 中,我们介绍了如何应用 JSFTemplating 编写 JavaServer Faces 组件。这篇文章提供了一种开发 JavaServer Faces 组件呈现器的简单方法,可以将 Java 代码中的组件标记移至一个模板文件并显著改善组件清晰度和可维护性。
本文讨论的方法虽然实用,但是无法解决 JavaServer Faces 组件创建过程中遇到的其他众多问题。例如,您仍然需要编写 JavaServer Pages (JSP) 标记处理程序、JSP TLD 文件、faces-config.xml 文件,并且还可能需要 Facelets taglib.xml 文件。此外,还必须通过某种方式打包并处理与组件有关的资源。
本文对上篇文章进行了扩展,展示了一种更简单的方法来解决其余的组件创建问题。通过遵循本文介绍的策略,JavaServer Faces 组件创建将变得轻而易举。
目录
除 GlassFish 应用服务器外,本文还将用到其他两个 GlassFish 开源项目的成果:Templating for JavaServer Faces Technology (JSFTemplating) 和 Woodstock。
借助这两个项目,只需使用两个文件即可编写一个 JavaServer Faces 组件:一个经过注释的 UIComponent Java 文件和一个模板文件。没错 — 只需要两个文件!
本文构建的组件将封装一个 来自 Yahoo! 的滑块部件,从而完善 UIInput JavaServer Faces 组件的功能。除组件本身需要的两个源文件外,还需要使用一些资源文件(JavaScript 文件、映像等等)。对资源功用的讨论超出了本文的范围;然而,本文将简要介绍资源文件的绑定和处理方式,从而为 JavaServer Faces 组件创建提供完整的解决方案。
要查看组件的运行情况,请参见下图,其中展示了示例应用程序组件的屏幕截图。
图 1. 示例应用程序中的组件
单击 此处 查看大图。
|
该图显示了两个滑块组件。每个滑块组件具有两个输入框,在滑块移动时由 JavaScript 代码更新。输入框的作用是演示滑块组件的功能。它们不属于滑块组件,并且不需要保持滑块组件的值—滑块组件本身就是一种输入组件。例如,水平滑块的标记及其相关输入框如下所示:
<div style="padding: 20px 0px 40px 50px;"> <p>The current value is #{(pageSession.slider1Value == null) ? "not set" : pageSession.slider1Value}.</p> <h:form id="form"> <sc:slider id="slider" min="0" max="250" orientation="horizontal" value="#{pageSession.slider1Value}" for="form:input1,form:input2" /> <br />
<h:outputLabel for="input1">Input #1</h:outputLabel> <h:inputText id="input1" /> <br /> <h:outputLabel for="input2">Input #2</h:outputLabel> <h:inputText id="input2" />
<br /> <h:commandButton value=" Click Me " /> </h:form> </div>
|
单击 此处 下载 ezcomp 示例应用程序的压缩文件。解压缩该文件并阅读 README.txt 文件,该文件描述组件需要的源文件以及创建所需的构建环境。
UIComponent 组件
查看该 JavaServer Faces 组件(或其他组件)的主要类 UIComponment,开始对组件进行分析。本文所述方法需要使用来自 JSFTemplating 的一个基类来帮助查找相关的模板文件。由于滑块是一种输入组件,它使用 TemplateInputComponentBase 提供基本的功能。更多细节,请参考 javadoc。
滑块的 UIComponent 使用的 Java 代码如下所示:要获得该文件,可通过以下位置访问示例 Java 归档文件(jar 文件):src/java/main/com/sun/faces/mojarra/component/YuiSlider.java。
package com.sun.faces.mojarra.component;
import com.sun.faces.annotation.Component; import com.sun.faces.annotation.Property; import com.sun.jsftemplating.annotation.Handler; import com.sun.jsftemplating.annotation.HandlerInput; import com.sun.jsftemplating.component.TemplateInputComponentBase; import com.sun.jsftemplating.layout.descriptors.handler.HandlerContext;
import javax.faces.context.FacesContext; import javax.faces.component.UIComponent;
/** * @author Jason Lee */
@Component(rendererClass = "com.sun.jsftemplating.renderer.TemplateRenderer", tagRendererType = YuiSlider.RENDERER_TYPE, type = YuiSlider.COMPONENT_FAMILY, family = YuiSlider.COMPONENT_FAMILY, displayName = "Slider", tagName = "slider")
public class YuiSlider extends TemplateInputComponentBase {
/** * <p>The standard component orientation for this component. </p> */ public static final String COMPONENT_FAMILY = "com.sun.faces.mojarra.YuiSlider";
/** * <p>The standard component family for this component.</p>
*/ public static final String RENDERER_TYPE = "com.sun.faces.mojarra.YuiSliderRenderer";
private Boolean animate = Boolean.TRUE; private double animationDuration = 0.2; private Boolean backgroundEnabled = Boolean.TRUE; private int min = 0; private Boolean enableKeys = Boolean.TRUE; private int keyIncrement = 10; private double scaleFactor = 1.0; private int max = 100; private int tick = 1; private String orientation = "horizontal"; private String forField; private Object[] _state = null;
public YuiSlider() { super(); setRendererType(RENDERER_TYPE); setLayoutDefinitionKey("templates/slider.xhtml"); }
public String getFamily() { return COMPONENT_FAMILY; }
public Boolean getAnimate() { return getPropertyValue(animate, "animate", Boolean.TRUE); }
public double getAnimationDuration() { return getPropertyValue(animationDuration, "animationDuration", 0.2); }
public Boolean getBackgroundEnabled() { return getPropertyValue(backgroundEnabled, "backgroundEnabled", Boolean.TRUE); }
public int getMin() { return getPropertyValue(min, "min", 0); }
public Boolean getEnableKeys() { return getPropertyValue(enableKeys, "enableKeys", Boolean.TRUE); }
public int getKeyIncrement() { return getPropertyValue(keyIncrement, "keyIncrement", 10); }
public double getScaleFactor() { return getPropertyValue(scaleFactor, "scaleFactor", 1.0); }
public int getMax() { return getPropertyValue(max, "max", 100); }
public int getTick() { return getPropertyValue(tick, "tick", 1); }
public String getOrientation() { return getPropertyValue(orientation, "orientation", "horizontal"); }
public String getFor() { return getPropertyValue(forField, "for", null); }
@Property(name = "animate") public void setAnimate(Boolean animate) { this.animate = animate; }
@Property(name = "animationDuration") public void setAnimationDuration(double animationDuration) { this.animationDuration = animationDuration; }
@Property(name = "backgroundEnabled") public void setBackgroundEnabled(Boolean backgroundEnabled) { this.backgroundEnabled = backgroundEnabled; }
@Property(name = "min") public void setMin(int limit) { this.min = limit; }
@Property(name = "enableKeys") public void setEnableKeys(Boolean enableKeys) { this.enableKeys = enableKeys; }
@Property(name = "keyIncrement") public void setKeyIncrement(int keyIncrement) { this.keyIncrement = keyIncrement; }
@Property(name = "scaleFactor") public void setScaleFactor(double scaleFactor) { this.scaleFactor = scaleFactor; }
@Property(name = "max") public void setMax(int limit) { this.max = limit; }
@Property(name = "tick") public void setTick(int tick) { this.tick = tick; }
/** * If the orientation starts with a 'v' or 'V', * set the orientation to 'vertical'. * Otherwise, default to 'horizontal'. */ @Property(name = "orientation") public void setOrientation(String orientation) { if ((orientation.charAt(0) == 'v') || (orientation.charAt(0) == 'V')) { this.orientation = "vertical"; } else { this.orientation = "horizontal"; } }
@Property(name = "for") public void setFor(String forField) { this.forField = forField; }
public void restoreState(FacesContext _context, Object _state) { this._state = (Object[]) _state; super.restoreState(_context, this._state[0]); animate = (Boolean) this._state[1]; animationDuration = (Double) this._state[2]; backgroundEnabled = (Boolean) this._state[3]; min = (Integer) this._state[4]; enableKeys = (Boolean) this._state[5]; keyIncrement = (Integer) this._state[6]; scaleFactor = (Double) this._state[7]; max = (Integer) this._state[8]; tick = (Integer) this._state[9]; orientation = (String) this._state[10]; forField = (String) this._state[11]; }
public Object saveState(FacesContext _context) { if (_state == null) { _state = new Object[12]; } _state[0] = super.saveState(_context); _state[1] = animate; _state[2] = animationDuration; _state[3] = backgroundEnabled; _state[4] = min; _state[5] = enableKeys; _state[6] = keyIncrement; _state[7] = scaleFactor; _state[8] = max; _state[9] = tick; _state[10] = orientation; _state[11] = forField; return _state; }
}
|
如果曾经编写过 JavaServer Faces 组件,那么应该对这些代码很熟悉:然而,需要注意三个不同之处:
- 在这个类中,文件开始部分是一个
@Component 注释,如下所示:
@Component(rendererClass = "com.sun.jsftemplating.renderer.TemplateRenderer", tagRendererType = YuiSlider.RENDERER_TYPE, type = YuiSlider.COMPONENT_FAMILY, family = YuiSlider.COMPONENT_FAMILY, displayName = "Slider", tagName = "slider")
|
该注释向 JavaServer Faces 提供了组件信息。具体来讲,它定义了以下元数据:
- Renderer Java class – rendererClass,该类始终是基于模板的组件的 TemplateRenderer
- Renderer Type - tagRenderType
- Component Type – 类型
- Component Family – 系列
- Display Name – displayName,工具支持的显示名称
- JavaServer Pages Tag Name – tagName
这些元数据提供了配置组件所需的大部分信息。Annotation Processing Tool (APT) 将使用这些信息生成 faces-config、taglib 和其他必要文件,如果使用注释,则可以忽略这些文件。
- 在组件的构造函数中,需要指定用来呈现组件的模板文件。
setLayoutDefinitionKey("templates/slider.xhtml");
|
组件将首先在应用程序的 docroot 中查找这个模板,这样做可以简化开发,因为当模板发生修改后,可立即将修改反映到组件中。如果没有找到模板文件,组件将搜索类路径(类加载程序将缓存文件以防动态重新加载,因此不会产生性能影响)。在示例应用程序中,文件位于 docroot 而不是 jar 文件中,因此可对其进行试验。
@Property 注释将应用于组件提供的所有属性。这些属性可用于创建 JSP taglib 文件。
@Property(name = "animate")
|
该文件还包含了典型的 UIComponent 代码。如果不需要组件支持,可以删除组件中的属性并使用 JavaServer Faces 属性图 — 从 此处 查阅使用该方法的文章。如果使用 JavaServer Faces 属性图,那么就不需要用到文件的其余内容 — 状态保存代码以及所有 getter 和 setter 方法。
模板
本节将描述第二个也是最后一个必需文件,即模板文件。示例组件将演示 JSFTemplating 功能以使用 Facelets 语法。很多人(包括模板文件的作者 Jason Lee)都非常熟悉这种语法。
上一节描述的 UIComponent 文件指定了模板文件的位置:templates/slider.xhtml。您应该能够在示例 jar 文件中找到它,该文件应该类似于以下代码示例。
注意,示例应用程序中的“page”也称为 slider.xhtml 并且位于 docroot 中 — 这是另一个文件!
<html xmlns="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:event type="decode" > ezcomp.decode(value="$requestParameter{$this{clientId}}"); </ui:event >
<ui:composition >
<ui:include src="templates/init.xhtml"/ > <link rel="stylesheet" type="text/css" href="#{baseUrl}yui/container/assets/container.css"/ > <script type="text/javascript" src="#{baseUrl}yui/slider/slider-min.js" > </script > <script src="#{baseUrl}yui/animation/animation-min.js" > </script > <script src="#{baseUrl}yui/container/container-min.js" > </script >
<ui:include src="templates/cssOverrides.xhtml"/ >
<span class="yui-skin-sam" > <input type="hidden" id="$this{clientId}" name="$this{clientId}" value="$property{value}" / > <div id="$this{clientId}_slider" style="background-color: #990000; background:url('#{baseUrl}scales/img/slider-bg-$property{orientation}.gif'); background-repeat: repeat-#{'$property{orientation}' == 'horizontal' ? 'x' : 'y'}; #{'$property{orientation}' == 'horizontal' ? 'height' : 'width'}: 28px; #{'$property{orientation}' == 'horizontal' ? 'width' : 'height'}: #{($property{max} - $property{min}) * $property{scaleFactor} + 14}px;" > <div id="$this{clientId}_sliderthumb" >
<img src="#{baseUrl}scales/img/slider-thumb-$property{orientation}.gif"/ > </div > </div > <script type="text/javascript" > YAHOO.util.Event.onDOMReady(function() { var slider_$this{id} = YAHOO.widget.Slider.get#{'$property{orientation}' == 'horizontal' ? 'Horiz' : 'Vert'}Slider( "$this{clientId}_slider", "$this{clientId}_sliderthumb", $property{min}, $property{max}, $property{tick}); slider_$this{id}.getRealValue = function() { return Math.round(this.getValue() * $property{scaleFactor}); }
// Subscribe to the onChange event to capture the new value from the slider slider_$this{id}.subscribe("change", function(offsetFromStart) { YAHOO.util.Dom.get('$this{clientId}').value = this.getRealValue(); // update hidden field YAHOO.util.Dom.get('$this{clientId}_slider').title = this.getRealValue(); // Update the slider's div's title
// Update any input fields that might be tied to this slider //var elems = YAHOO.util.Dom.getElementsByClassName('bd', 'div', "slider_$this{id}_tooltip"); //elems[0].innerHTML = slider_$this{id}.getRealValue(); for(var i=0;i != this.ids.length;i++) { var elem = YAHOO.util.Dom.get(this.ids[i]); if (elem != null) { elem.value = this.getRealValue(); } } }, slider_$this{id}, true); slider_$this{id}.setValue($property{value});
// If the "for" property was specified, spilt the value on the comman // and store the array on the slider object var fields="$property{for}"; if (fields != null) { if (fields.length != 0) { slider_$this{id}.ids = fields.split(","); } } }); </script > </span >
</ui:composition > </html >
|
该文件的各个部分具有不同用途:
- 在接近文件顶部的位置有一个
decode 事件,它通知组件如何执行提交操作:
<ui:event type="decode"> ezcomp.decode(value="$requestParameter{$this{clientId}}"); </ui:event>
|
该事件用于 UIInput 类型的组件。在本例中,它将调用 ezcomp.decode 处理程序并传递以相同名称作为组件的 componentId 的请求参数。对于所有使用这种方式解码的输入组件,可以重复使用这个处理程序。这个处理程序的源代码在 src/java/main/com/sun/faces/mojarra/util/TemplateHandlers.java 文件中进行了定义。该文件不在本文讨论范围之内。
- 注意这两条
<ui:include> 语句。其中包含的内容可以在 Jason Lee 编写的其他组件中共享,并轻松实现内联。如果觉得好奇可以进一步查看,但是本文不做过多说明。
- 注意 hidden 字段,组件使用该字段完成将值传递回服务器这一机制:
<input type="hidden" id="$this{clientId}" name="$this{clientId}" value="$property{value}" />
|
- 资源(JavaScript 代码和映像)通过特殊 URL 完成加载,该 URL 使用
#{baseURL} 作为前缀。变量 baseURL 在附带的 init.xhtml 文件中进行定义,此处不做说明。baseURL 的定义行类似下面的内容:
util.getStaticResourceUrl(path="", url=>$attribute{baseUrl});
|
该行是另一个 JSFTemplating 处理程序。基 URL 可以确保映像和 JavaScript URL 请求可被定制的 JavaServer Faces 阶段侦听器识别,从而可从 jar 文件中提取。本文稍后将进一步讨论这种方法。
文件的其余部分确定组件呈现布局,其外观类似 Facelets 风格的页面。
- 最后查看该文件的最佳特性:模板文件可以进行动态修改。作出修改,重新加载到浏览器中,您可以立即看到所做修改。使用基于 Java 的 JavaServer Faces Renderer 尝试一下这个特性吧!
编译
现在,已经完成了 JavaServer Faces 组件的定义。但是,您仍然需要编译组件以便编译 Java 文件并处理注释。使用本文介绍的方法,不需要动手编写就可以生成所有编译内容。示例应用程序的 ant build.xml 文件定义了编译流程:
<!-- This target builds the files and processes any annotations --> <target name="compile" description="Compile the project."> <mkdir dir="${build}/." />
<!-- Compile the java code from ${src} into ${build} --> <apt srcdir="${src}" preprocessdir="${generated-source-dir}" fork="true" destdir="${build}/." debug="${compile.debug}" deprecation="${compile.deprecation}" optimize="${compile.optimize}"> <option name="generate.runtime" value="" /> <option name="namespace.uri" value="${taglib-uri}"/> <option name="namespace.prefix" value="${taglib-prefix}"/> <option name="taglibdoc" value="src/java/conf/tag-descriptions.xml"/>
<classpath refid="dependencies" /> </apt> <copy file="${build}/taglib.xml" tofile="${build}/ezcomp.tld"/> </target>
|
该文件要求 ant 1.7 处理 <apt> 任务定义。总体来讲,文件逻辑非常简单:编译代码,然后完成。有关将类归档到 jar 文件并创建 war 文件等其他目标,请查看 build.xml 文件。这些内容不在本文讨论范围之内。
在执行目标时,API 将处理注释并生成如下文件:
src/build/faces-config.xml – 定义组件及其呈现程序以便 JavaServer Faces 了解如何进行创建和显示
src/build/facelets.taglib.xml – 供 Facelets 和 JSFTemplating 使用,以便可以在页面模板中使用组件
src/build/taglib.xml – 用于 JavaServer Pages JSF 文件
src/build/com/sun/faces/mojarra/component/YuiSliderTag.class – 也用于 JavaServer Pages JSF 文件
src/build/META-INF/jsftemplating/Handler.map – 是一个 JSFTemplating 文件,包含用于模板文件的处理程序的配置信息。
src/gensrc/com/sun/faces/mojarra/component/YuiSliderTag.java – 滑块的 UIComponent
现在理解了 build.xml 文件之后,可以通过输入 ant 命令执行该文件 — 假设您遵循本文开始部分提及的 README.txt 文件中的说明。README.txt 文件说明如何编辑 build.properties 文件以适合您的环境。如果一切设置正确,将在几秒之内完成任务。
运行示例应用程序
现在可以开始部署应用程序了。如果对应用程序执行“字典部署”,那么可以直接编辑源文件并在浏览器中即时查看。要在 asadmin 命令行界面中使用 GlassFish 进行字典部署,输入以下命令:
glassfish-home/bin/asadmin deploydir -p 4848 --contextroot ezcomp path-to-directory
|
其中,glassfish-home 是 GlassFish 的安装目录,而 path-to-directory 是 ezcomp 示例应用程序的路径。
您还可以通过 GlassFish Admin Console 执行应用程序字典部署,如下图所示。
图 2。通过 GlassFish Admin Console 进行部署
单击 此处 查看大图。
|
应用程序部署完毕后,将浏览器导航到 http://localhost:8080/ezcomp 进行试验。应该能够看到如下图所示的页面:
图 3。示例应用程序初始页面
单击 此处 查看大图。
|
该页面提供了两种选择:使用 JSFTemplating 或 Facelets 运行 slider.xhtml 页面。该选项说明:即使页面其他位置没有使用 JSFTemplating,您也可以使用 JSFTemplating 组件 — 该组件可以在任何 JavaServer Faces 环境中运行。示例应用程序首页提供的这两种选择都可以访问磁盘中的文件,但是,应用程序配置了两种不同的扩展,因此可以同时运行 JSFTemplating 和 Facelets. 不论单击哪个链接,都将看到类似 图 1 所示的页面。
请记住,您可以在磁盘上修改页面或组件 xhtml 文件并在浏览器中即时查看修改。警告:提交表单后,将从表单(或会话)中恢复修改状态,而不是从磁盘恢复。要重新加载页面,最安全的方法是单击浏览器地址栏中的 Go To Address 按钮。
资源解析
现在,您已经完成了组件部署,可以开始进行打包并与其他人分享。对组件进行打包非常简单:只需将所有编译过的类文件、模板、组件资源等全部包含到 jar 文件中,然后将生成的 faces-config.xml 放到 jar 文件根目录下的 META-INF 目录。尽管对资源进行打包比较简单,但是将组件所需的资源(例如映像、Javascript 文件和 css 文件)提供给浏览器就较为复杂。
要解决资源供给问题,有若干解决方案可供选择。JSFTemplating 提供了一个非常高效的 FileStreamer 服务(参见 javadoc),它提供了使用 JSF ViewHandler 的资源。Shale Remoting 提供了类似的功能,但是使用了阶段侦听器。一些方法使用了一个 servlet,但是 servelet 需要一个 web.xml 入口,这一点并不理想。其他项目使用其他方式解决这一问题。
在本文的示例中,Jason Lee 决定使用他以前编写的阶段侦听器,从而代替 Ken Paulsen 的 JSFTemplating 方法,该侦听器曾在他的其他组件中使用过。可通过位于 src/java/main/com/sun/faces/mojarra/util/StaticResourcePhaseListener.java 的源代码树找到这个阶段侦听器。. 模板文件中为每个资源指定的 URL 就是专门针对这个阶段侦听器,因此可以快速从 jar 文件中提取资源,而不是将请求作为普通的 JavaServer Faces 请求进行处理。
在本文中,您了解了如何使用 GlassFish JSFTemplating 和 Project Woodstock 提供的解决方案简化 JavaServer Faces 组件的开发。希望 Project Scales 在未来能够托管更多此类型的组件。访问该项目并贡献您自己的开源组件。
|