|
By Ken Paulsen, Jason Lee and Rick Palkovic, December 2007
|
|
|
Articles Index
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
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.
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
Click here for
a larger image. |
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.
The UIComponent
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 JSP taglib 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.
The Template
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="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 >
|
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 file
src/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 variable baseURL is
defined in the included init.xhtml file, not shown here.
The line that defines baseURL 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!
Building
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 it
-
src/build/facelets.taglib.xml – Used by Facelets and
JSFTemplating so that you can use the component in page templates
-
src/build/taglib.xml – Used for JavaServer Pages JSF files
-
src/build/com/sun/faces/mojarra/component/YuiSliderTag.class –
Also used for JavaServer Pages JSF files
-
src/build/META-INF/jsftemplating/Handler.map – A JSFTemplating
file that contains the configuration information for the Handlers used in
the template file
-
src/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.
Running the Demo Application
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
Click here for
a larger image. |
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
Click here for
a larger image. |
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.
Resource Resolution
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.
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.
|
Ken Paulsen is a senior staff
engineer at Sun Microsystems. He works on Project GlassFish and is also a
member of the JSR 314 expert group (JSF 2.0). Ken is the founder of the
JSFTemplating project.
|
Jason Lee has been writing
software professionally since 1997 in a wide variety of languages and
environments. He is a software architect for Objectstream, an Oklahoma-based
IT consulting firm, is a JSR 314 (JSF 2.0)
expert group member, and is president of the Oklahoma City JUG.
|
Rick Palkovic is a staff writer for Sun Developer Network. He has written about Solaris and Java technologies for longer than he likes to admit, composing everything from man pages to technical white papers.
|
|