Technical Article
JSFTemplating and Woodstock: Component Authoring Made Easy
By Ken Paulsen, Jason Lee and Rick Palkovic, December 2007
In a recent article, JSFTemplating was applied to writing JavaServer Faces components. That article presents a simple way to develop a JavaServer Faces component Renderer, moving the markup for a component from Java code to a template file and vastly improving the clarity and maintainability of the component.
The approach discussed in that article is helpful, but it doesn't address many of the other pain points of JavaServer Faces component authoring. For example, you must still write a JavaServer Pages (JSP) tag handler, the JSP TLD file, the faces-config.xml
file, and potentially the Facelets taglib.xml
file. You must also find a way to package and serve resources associated with the component.
This article extends the approach to show an easier way to solve the rest of the component authoring problem. If you follow the strategy outlined here, JavaServer Faces component authoring is much less frustrating.
Contents
Component Development with Two Files
In addition to the GlassFish application server, this article uses the work of two other GlassFish open source projects: Templating for JavaServer Faces Technology (JSFTemplating) and Woodstock.
- Templating for JavaServer Faces Technology – The goal of JSFTemplating is to work with JavaServer Faces technology to make building pages and components easier. In this article, JSFTemplating is used to define the layout of an example component.
- Woodstock – The goal of Project Woodstock is to develop the next generation of user interface components for the web, based on JavaServer Faces and AJAX technologies. This article borrows the annotation code defined in the project to help build the example component.
With the help of these two projects, you can write a JavaServer Faces component with only two files: an annotated UIComponent Java file, and a template file. That's right — only two files!
The component you build in this article wraps a slider widget from Yahoo! making it a complete UIInput JavaServer Faces component. In addition to the two source files required for the component itself, you need a number of resource files (JavaScript files, images, and so on). The function of these resources is outside the scope of this article; however, the article will briefly describe how the resource files are bundled and served to provide a complete solution for JavaServer Faces component authoring.
Component Demonstration Application
To see what the component looks like in action, see the following figure, which shows a screenshot of the component in the demo application.
Figure 1. Components in the Demo Application
The figure shows two slider components. Each slider component has two input boxes that are updated by JavaScript code as the slider moves. The input boxes are provided to illustrate the capabilities of the slider component. They are not part of the slider component and are not needed to hold the value of the slider component — the slider component is an input component all by itself. For example, the markup for the horizontal slider and associated input boxes is as follows:
<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>
Click here to download a zip file of the ezcomp demo application. Unzip the file and refer to the README.txt
file, which describes the included source for the component and the build environment for creating it.
Begin analyzing the component by taking a look at the UIComponent, the primary class for this (or any other) JavaServer Faces component. The approach described in this article requires a base class from JSFTemplating to assist in finding the associated template file. Because the slider is an input component, it uses TemplateInputComponentBase to provide base functionality. Consult the javadoc for details.
The following Java code is the slider's UIComponent. You can find this file in the demo Java archive (jar file) in the following location: 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;
}
}
If you have written a JavaServer Faces component before, the code above should look familiar. However, three things are different:
- Near the top of the file is a
@Component
annotation for this class, reproduced below:@Component(rendererClass = "com.sun.jsftemplating.renderer.TemplateRenderer", tagRendererType = YuiSlider.RENDERER_TYPE, type = YuiSlider.COMPONENT_FAMILY, family = YuiSlider.COMPONENT_FAMILY, displayName = "Slider", tagName = "slider")
This annotation provides information to JavaServer Faces technology about the component. Specifically, it defines the following metadata:
- Renderer Java class – rendererClass, which is always TemplateRenderer for a template-based component
- Renderer Type – tagRendererType
- Component Type – type
- Component Family – family
- Display Name – displayName, a display name for tool support
- JavaServer Pages Tag Name – tagName
- This metadata provides most of the information needed to configure the component. The information is used by the Annotation Processing Tool (APT) to generate the
faces-config
,taglib
, and other required files that you can ignore if you use the annotation. - In the constructor of the component, you specify the template file to be used to render the component.
setLayoutDefinitionKey("templates/slider.xhtml");
The component first looks for this template in the docroot of the application, a behavior that makes development easy because changes appear instantly in the component when the template is changed. If the template file is not found there, the component searches the classpath (the classloader caches files to prevent dynamic reloading, so there is no performance penalty). In the demo application, the file is located in the docroot instead of inside a jar file so you can experiment with it.
@Property
annotations are used on all the properties the component provides. These are used for creating the JSPtaglib
file.@Property(name = "animate")
The rest of the file contains typical UIComponent code. If you don't need tool support, you can eliminate the properties in the component and instead rely on the JavaServer Faces attribute map — you can find an article here that uses that approach. Relying on the JavaServer Faces attribute map would eliminate the need for state-saving code and all the getters and setters that occupy the remainder of the file.
This section describes the second and final required file, the template. The example component demonstrates the JSFTemplating ability to use the Facelets syntax. Many people (including Jason Lee, the author of the template file) are familiar with this syntax.
The UIComponent file described in the last section specified the location of the template file: templates/slider.xhtml
. You should be able to find it there in the demo jar file, and it should look like the following code example.
Note that the "page" in the demo application is also named slider.xhtml
and is in the docroot — that is a different file!
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="#"
xmlns:h="#"
xmlns:f="#" >
<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 >
Several parts of the file are of particular interest:
- Near the top of the file you see a
decode
event, which tells the component what to do on submit.<ui:event type="decode"> ezcomp.decode(value="$requestParameter{$this{clientId}}"); </ui:event>
This event is needed for UIInput-type components. In this case, it calls the
ezcomp.decode
handler and passes in a request parameter with the same name as the componentId for the component. This reusable handler can be used for all input components that behave this way on decode. The source for this handler is defined in the filesrc/java/main/com/sun/faces/mojarra/util/TemplateHandlers.java
. This article does not discuss this file. - Note the two
<ui:include>
statements. They include content that is shared with other components Jason Lee has written, and could be in-lined rather than included just as easily. Take a look at these if you are curious — they won't be further explained in this article, though. - Note the hidden field that the component uses to do the mechanics of passing the value back to the server:
<input type="hidden" id="$this{clientId}" name="$this{clientId}" value="$property{value}" />
- Resources (JavaScript code and images) are loaded with a special URL, prefixed with
#{baseURL}
. The variablebaseURL
is defined in the includedinit.xhtml
file, not shown here. The line that definesbaseURL
is similar to the following:util.getStaticResourceUrl(path="", url=>$attribute{baseUrl});
This line is another JSFTemplating handler. The base URL ensures that the image and JavaScript URL requests are identified by a custom JavaServer Faces phase listener so that they can be served from a jar file. This approach is discussed again later in the article.
The rest of the file is the layout that is needed to render the component, which looks like a Facelets-style page.
- And to save the best notable feature for last: the template file can be changed on the fly. Make a change, reload it in your browser, and you can see the change instantly. Try that with a Java-based JavaServer Faces Renderer!
Your JavaServer Faces component is now defined. However, you still need to build it so that the Java file can be compiled and the annotations can be processed. With this article's approach, the compile step generates everything you didn't need to write by hand. The demo application's ant build.xml
file defines the build process:
<!-- 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>
The file requires ant 1.7 to process the <apt>
task definition. Overall, the logic of the file is straightforward: it compiles the code and finishes. See the build.xml
file for additional targets that archive the classes into jar files and create a war file. Discussion of those targets is outside the scope of this article.
When the target executes, APT processes the annotations and generates the following files:
src/build/faces-config.xml
– Defines the component and its renderer so JavaServer Faces knows how to create and display itsrc/build/facelets.taglib.xml
– Used by Facelets and JSFTemplating so that you can use the component in page templatessrc/build/taglib.xml
– Used for JavaServer Pages JSF filessrc/build/com/sun/faces/mojarra/component/YuiSliderTag.class
– Also used for JavaServer Pages JSF filessrc/build/META-INF/jsftemplating/Handler.map
– A JSFTemplating file that contains the configuration information for the Handlers used in the template filesrc/gensrc/com/sun/faces/mojarra/component/YuiSliderTag.java
– The slider's UIComponent
Now that you understand the build.xml
file, you can execute it by typing the ant
command — assuming that you followed instructions in the README.txt
file mentioned at the beginning of this article. The README.txt
file explains how to edit the build.properties
file to suit your environment. If all is set up correctly, the task should complete in a few seconds.
You are now ready to deploy the demo application. If you "directory deploy" the application, you can edit the source files in place and see them live in your browser. To directory deploy with GlassFish from the asadmin
command-line interface, type the following command:
glassfish-home/bin/asadmin deploydir -p 4848 --contextroot ezcomp path-to-directory
where glassfish-home
is the GlassFish installation directory, and path-to-directory
is the path to the ezcomp
demo application.
You can also directory deploy the application from the GlassFish Admin Console, as shown in the following figure.
Figure 2. Deploying from the GlassFish Admin Console
After you've deployed the application, you can try it out by using your browser to navigate to http://localhost:8080/ezcomp
. You should see a page like the the one shown in the following figure:
Figure 3. Demo Application Initial Page
The page provides two choices: run the slider.xhtml
page with JSFTemplating or with Facelets. The choices highlight the fact that you can use a JSFTemplating component even if JSFTemplating is not used elsewhere in the page — the component works in any JavaServer Faces environment. Either choice you make from the demo application's front page accesses the same file on disk, but the application is configured with two different extensions so that you can run JSFTemplating and Facelets side-by-side. Whichever link you click, you will see a page similar to Figure 1.
Remember that you can change the page or component xhtml files on disk and view the changes immediately in the browser. One caveat: if you've submitted a form, a change will restore the state from the form (or session) instead of starting over from disk. To reload the page, the safest method is to click the Go To Address button in your browser's location bar.
You have finished developing your component and are ready to package it and share it with others. Packaging the component is easy: all you have to do is include all of the compiled class files, templates, component resources, and so on, in the jar file, and put the generated faces-config.xml
in the META-INF
directory in the root of the jar. Although packaging all of these resources is easy, getting the resources needed by your component, such as images, Javascript files, and css files to the browser is a bit more difficult.
To solve the problem of providing resources, a number of solutions are available. JSFTemplating provides a very efficient FileStreamer service (see javadoc) that provides resources using the JSF ViewHandler. Shale Remoting does something similar but uses a phase listener. Some approaches use a servlet, although a servelet requires a web.xml
entry, which is undesirable. Other projects have solved this issue in other ways.
In the example used here, Jason Lee decided not to use Ken Paulsen's JSFTemplating method but instead to use a phase listener that he had previously written and had used in his other components. His phase listener can be found in the source tree at: src/java/main/com/sun/faces/mojarra/util/StaticResourcePhaseListener.java
. The URL specified for each resource in the template file is targeted to this phase listener so that it immediately serves the resource out of a jar file instead of processing the request as a normal JavaServer Faces request.
Summary
In this article, you saw how solutions provided by GlassFish JSFTemplating and Project Woodstock can be used to make JavaServer Faces component development much more enjoyable. In the future, expect Project Scales to host more of these types of components. Contribute yourself by requesting access to the project and contributing your own open source components.