Recall from Part 1 that JavaServer Faces (JSF) is conceptually a mixture of Struts (Apache's popular open source JSP framework) and Swing (Java's standard desktop application framework). Like Struts, JSF provides a well-defined lifecycle, shown in Figure 1, for processing requests.
In Part 1, the JSF lifecycle was discussed in detail and therefore is not discussed here; however, this article refers to the JSF lifecycle, and Figure 1 is repeated for convenience.
Like Swing, JSF specifies a rich component hierarchy for implementing server-side user interface (UI) components. That hierarchy lets developers implement components from scratch with various renderers that can render different markup languages. The JSF component hierarchy and related objects such as validators, event handlers, and model objects are the focus of this article's following sections:
Read the whole series, "A First Look at JavaServer Faces:"
Figure 2 shows the class diagram for JSF components.
JSF components implement the javax.faces.component.UIComponent
interface, which specifies a whopping 46 methods that define the essence of a JSF component; Figure 2 lists only a handful of those methods. Luckily, your components can extend the abstract
class javax.faces.component.UIComponentBase
, which implements sane defaults for all UIComponent
methods except for getComponentType()
. That means you can implement a custom component simply by extending UIComponentBase
and implementing getComponentType()
.
Each JSF component has:
All JSF components are potential containers, by virtue of a child components list maintained by every JSF component. That lets you nest components—even if they contain other components—inside other components. This crucial ability, which is the impetus behind the Composite design pattern, is routinely implemented by object-oriented graphical user interfaces (GUIs), such as Swing or VisualWorks Smalltalk.
JSF components also maintain a list of attributes. Those attributes store component-specific information. For example, you may want to store the URL associated with an image used by a component; so you could store that URL—or the image itself—in the component's list of attributes. Component attributes are stored by name in a hash map.
All JSF components perform three fundamental tasks:
JSF components can render themselves or delegate rendering to a renderer. The boolean UIComponent.rendersSelf()
method tells the JSF implementation whether or not a component renders itself; if not, the JSF implementation obtains a reference to the component's renderer with the UIComponent.getRendererType()
method's help and then calls on the renderer to produce markup for the component.
JSF component event handling can also be managed directly by a component, or components can delegate to an event handler. One or more event handlers can be registered for a component, typically by a component's renderer or the component itself, during the JSF lifecycle's Apply Request Values phase.
Finally, JSF components can have one or more validators that validate input. Those validators, which are usually created by the JSF implementation, are stored by components in an array list.
Now that we have a basic understanding of JSF components, let's look at implementing a custom validator and associating it with a component.
In Part 1, we discussed using built-in validators to validate input for JSF components. If you use JSP for your Web application's views—which is typically the case—you can specify validators for JSF components with a <faces:validator>
tag, like this:
<faces:textentry_input id='name'> <faces:validator className='javax.faces.validator.RequiredValidator'/></faces:textentry_input>
The code fragment attaches a validator to a text field; you just specify the validator's class name with the <faces:validate>
tag's className
attribute. The validator specified above is a JSF built-in validator that checks to make sure a component's value is not null
. As discussed in Part 1, JSF provides a handful of built-in validators, but you can also implement your own validators and associate them with a JSF component. For example, the application shown in Figure 3 uses a custom validator to validate a username.
The JSP page displayed in Figure 3 is listed in Listing 1.
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'><html> <head> <title>A Simple JavaServer Faces Application</title> </head> <body> <%@ taglib uri='http://java.sun.com/j2ee/html_basic/' prefix='faces' %> <font size='4'>Please enter your name and password</font> <faces:usefaces> <faces:form id='simpleForm' formName='simpleForm'> <table> <tr> <td>Name:</td> <td> <faces:textentry_input id='name'> <faces:validator className='com.sabreware.validators.NameValidator'/> <faces:validator className='javax.faces.validator.LengthValidator'/> <faces:attribute name='javax.faces.validator.LengthValidator.MINIMUM' value='3'/> </faces:textentry_input> </td> <td> <faces:validation_message componentId='name'/> </td> </tr> <tr> <td>Password:</td> <td> <faces:textentry_secret id='password'/> </td> </tr> </table> <p><faces:command_button id='submit' commandName='Log In'/> </faces:form> </faces:usefaces> </body></html>
Like the code fragment shown at the beginning of this section, the preceding JSP page uses <faces:validator>
tags to attach validators to a JSF component. In this case, the validators are a custom validator that authenticates a username and a JSF built-in validator that checks to make sure the username is at least three characters long.
The custom validator used in Listing 1 is listed in Listing 2.
package com.sabreware.validators;import java.util.Iterator;import javax.faces.component.UIComponent;import javax.faces.component.AttributeDescriptor;import javax.faces.context.FacesContext;import javax.faces.context.Message;import javax.faces.context.MessageImpl;import javax.faces.validator.Validator;public class NameValidator implements Validator { public AttributeDescriptor getAttributeDescriptor(String attributeName) { return null; } public Iterator getAttributeNames() { return null; } public void validate(FacesContext context, UIComponent component) { String name = (String)component.getValue(); if(!"phillip".equalsIgnoreCase(name)) { context.addMessage(component, new MessageImpl(Message.SEVERITY_ERROR, "bad username", "The username " + name + " is invalid")); } }}
The preceding validator implements the javax.faces.validator.Validator
interface, which defines the methods listed below:
void validate(FacesContext, UIComponent)
Iterator getAttributeNames(String)
AttributeDescriptor getAttributeDescriptor(String)
The validate()
method performs the actual validation for a given component. The other two methods, defined by the Validator
interface, are used by tools to discover attributes (and their descriptions) associated with a particular validator. In this case, we don't have any attributes for our validator, so the getAttributeDescriptor()
and getAttributeNames()
methods simply return null
.
A validator's validate()
method does nothing if the component's values are valid; if those values are invalid, the validator()
method creates messages and adds them to the JSF context. All of that happens during the JSF lifecycle's Process Validations phase. If a component fails validation—meaning messages have been added to the JSF context—the JSF implementation proceeds directly to the Render Response phase; otherwise, the lifecycle proceeds to the Apply Model Values phase. (Refer to Figure 1 for more information about the JSF lifecycle phases.)
Listing 3 shows the JSP page illustrated in Figure 4a.
SUBHEAD2: Listing 3. /index.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><html> <head> <title>A Simple JavaServer Faces Application</title> </head> <body> <%@ taglib uri="http://java.sun.com/j2ee/html_basic/" prefix="faces" %> <font size="4">Please enter your name and password</font> <!-- We use jsp:useBean to create a model object, but real applications typically create model objects elsewhere: JSP pages are purely views of the model --> <jsp:useBean id='user' class='com.sabreware.beans.User' scope='session'/> <faces:usefaces> <faces:form id="simpleForm" formName="simpleForm"> <table> <tr> <td>Name:</td> <td><faces:textentry_input id="name" modelReference="${user.name}"/></td> </tr> <tr> <td>Password:</td> <td><faces:textentry_secret id="password" modelReference="${user.password}"/></td> </tr> </table> <p><faces:command_button id="submit" commandName="Log In"/> </faces:form> </faces:usefaces> </body></html>
The User
class is demonstrated in Listing 4.
package com.sabreware.beans;public class User { private String name = null, password = null; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setPassword(String pwd) { this.password = pwd; } public String getPassword() { return password; }}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><html> <head> <title>A Simple JavaServer Faces Application</title> </head> <body> <%@ taglib uri='http://java.sun.com/jstl/core' prefix='c' %> Welcome to JavaServer Faces, <c:out value='${user.name}'/>! </body></html>
The preceding JSP page accesses the name
property of the user
object created by the JSP page listed in Listing 3. That property is accessed with the JSP Standard Tag Library (JSTL). Note the exact same syntax is used in Listing 3 to specify user properties with the modelReference
attribute.
Java-based Web applications typically localize messages (and format or parse numbers, currencies, percents, and dates) in JSP pages. It's also common to localize messages in a Java class. A validator, for example, may localize error messages. This section discusses both approaches using JSTL to internationalize the Web application discussed in the previous "Model Objects at Work" section.
First, we store all localized text in a properties file:
login.window-title=Internationalization Javaserver Faceslogin.name=Namelogin.password=Passwordlogin.submit=Log Inerrors.bad-username=Bad usernameerrors.bad-username-details=The username {0} is invalid
Then we rewrite Listing 3's JSP page to localize all text displayed to the user. Listing 7 shows that rewritten JSP page.
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'><html> <head> <%@ taglib uri='http://java.sun.com/jstl/fmt' prefix='fmt' %> <fmt:setLocale value='en-US' scope='application'/> <fmt:setBundle basename='messages' scope='application'/> <title><fmt:message key='login.window-title'/></title> </head> <body> <%@ taglib uri='http://java.sun.com/j2ee/html_basic/' prefix='faces' %> <font size='4'>Please enter your name and password</font> <jsp:useBean id='user' class='com.sabreware.beans.User' scope='application'/> <faces:usefaces> <faces:form id='simpleForm' formName='simpleForm'> <table> <tr> <td><fmt:message key='login.name'/></td> <td><faces:textentry_input id='name' modelReference='${user.name}'> <faces:validator className= 'com.sabreware.validators.NameValidator'/> </faces:textentry_input> </td> <td> <faces:validation_message componentId='name'/> </td> </tr> <tr> <td><fmt:message key='login.password'/></td> <td><faces:textentry_secret id='password' modelReference='${user.password}'/></td> </tr> </table> <p><faces:command_button id='submit' commandName='Log In'/> </faces:form> </faces:usefaces> </body></html>
The preceding JSP page uses two tags from the JSTL formatting library to set a locale and resource bundle base name (<fmt:setLocale>
and <fmt:setBundle>
, respectively) and a third tag (<fmt:message>
) to display localized messages stored in the specified resource bundle.
The validator listed below localizes error messages, also using JSTL.
package com.sabreware.validators;import java.util.*;import java.text.MessageFormat;import javax.servlet.ServletContext;import javax.servlet.jsp.jstl.core.Config;import javax.servlet.jsp.jstl.fmt.LocalizationContext;import javax.faces.component.UIComponent;import javax.faces.component.AttributeDescriptor;import javax.faces.context.FacesContext;import javax.faces.context.Message;import javax.faces.context.MessageImpl;import javax.faces.validator.Validator;public class NameValidator implements Validator { public AttributeDescriptor getAttributeDescriptor(String attributeName) { return null; } public Iterator getAttributeNames() { return null; } public void validate(FacesContext context, UIComponent component) { String name = (String)component.getValue(); if(!"phillip".equalsIgnoreCase(name)) { ServletContext app = context.getServletContext(); LocalizationContext lc = (LocalizationContext) Config.get(app, Config.FMT_LOCALIZATION_CONTEXT); if(lc == null) { context.addMessage(component, new MessageImpl(Message.SEVERITY_ERROR, "bad username", "The username " + name + " is invalid")); } else { ResourceBundle rb = lc.getResourceBundle(); Object[] args = new Object[] { new String(name) }; String cs = rb.getString("errors.bad-username-details"); String s = MessageFormat.format(cs, args); context.addMessage(component, new MessageImpl(Message.SEVERITY_ERROR, rb.getString("errors.bad-username"), s)); } } }}
JSTL maintains a localization context that contains a resource bundle and the locale used to locate that resource bundle. That localization context is available through the JSTL Config
class. The preceding JSP page uses the context's resource bundle to localize messages.
Before we move on to JSF custom components, you should note that internationalization support for JSF is not complete. In Listing 7, the text displayed by the Submit button is not localized because the JSF <command_button>
tag does not support text localization. We could have internationalized the text with a scriptlet, but that involves ugly code that should not exist in a JSP page. Internationalization support will be complete by JSF 1.0's release.
Note: Detailed coverage of JSTL internationalization is beyond the scope of this article. For more information about JSTL, see Resources.
Figure 5a shows the application after it starts; the two custom components display their primary images. Figure 5b shows the application after the user clicks on the left-hand component, and Figure 5c shows the application after the user subsequently clicks on the right-hand component. If you click on the components a second time, the components will display their original image.
These custom components might seem trivial and somewhat useless; however, even though the former is true (the components are purposely trivial to illustrate JSF component implementation), the latter is not necessarily valid—this custom component can be used as part of any component that contains clickable images. For example, this custom component could be used in a tree control to expand or shrink nodes displayed in the tree.
Listing 9 lists the JSP page shown in Figure 5a.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><html> <head> <title>Creating Custom Components with JavaServer Faces</title> </head> <body> <%@ taglib uri="http://java.sun.com/j2ee/html_basic/" prefix="faces" %> <%@ taglib uri="/WEB-INF/tlds/example.tld" prefix="sabreware" %> <faces:usefaces> <sabreware:toggleGraphic id='bananaKiwi' imageOne='/graphics/banana.jpg' imageTwo='/graphics/kiwi.jpg'/> <sabreware:toggleGraphic id='pineappleStrawberry' imageOne='/graphics/pineapple.jpg' imageTwo='/graphics/strawberry.jpg'/> </faces:usefaces> </body></html>
The preceding JSP page is simple—it uses a custom tag to create two instances of our custom component. For completeness, the tag library descriptor (TLD) associated with the <sabreware:toggleGraphic>
tag is listed in Listing 10.
<?xml version="1.0" encoding="ISO-8859-1" ?><!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"><taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>JSF Example</short-name> <display-name>A Simple JSF Example Tag</display-name> <description>This library contains one simple custom JSF tag</description> <tag> <name>toggleGraphic</name> <tag-class>com.sabreware.tags.ToggleGraphicTag</tag-class> <body-content>JSP</body-content> <description>A simple tag for a custom JSF component</description> <attribute> <name>imageOne</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>imageTwo</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag></taglib>
The <sabreware:toggleGraphic>
tag has three attributes: an identifier and the component's two images. Listing 11 shows the tag handler.
package com.sabreware.tags;import javax.faces.webapp.FacesTag;import javax.faces.component.UIComponent;import javax.faces.component.UIGraphic;import javax.faces.context.FacesContext;import javax.faces.event.FacesEvent;import javax.faces.event.RequestEventHandler;import com.sabreware.components.UIToggleGraphic;public class ToggleGraphicTag extends FacesTag { private String imageOne, imageTwo; public void setImageOne(String imageOne) { this.imageOne = imageOne; } public void setImageTwo(String imageTwo) { this.imageTwo = imageTwo; } public void overrideProperties(UIComponent component) { super.overrideProperties(component); component.setAttribute("imageOne", imageOne); component.setAttribute("imageTwo", imageTwo); } public UIComponent createComponent() { UIToggleGraphic comp = new UIToggleGraphic(); comp.setURL(imageOne); return comp; } public String getRendererType() { return null; }}
The preceding JSP custom tag extends the FacesTag
class from the javax.faces.webapp
directory. All custom tags that represent JSF components extend either FacesTag
or, if the tag needs to manipulate its body content, FacesBodyTag
.
Besides storing the component's images as class member variables, the preceding JSP custom tag also stores those images as properties of the tag's component in overrideProperties()
. The createComponent()
method, which is abstract and therefore must be implemented by all FacesTag
extensions, creates a component and returns a reference to that component. In the preceding tag, that method also sets the component's original image by calling the component's setURL()
method. Finally, the getRendererType()
method, which is also abstract in FacesTag
, returns a string constant that represents an identifier for the component's renderer. In this case, the component does not have a renderer; therefore, the getRendererType()
method returns null
.
The custom component is demonstrated in Listing 12.
package com.sabreware.components;import javax.servlet.http.HttpServletRequest;import javax.faces.component.UIGraphic;import javax.faces.context.FacesContext;import javax.faces.context.ResponseWriter;import javax.faces.event.CommandEvent;import javax.faces.event.FacesEvent;public class UIToggleGraphic extends UIGraphic { // This component supports one command: click. private static String clickCommand = "click"; // This method indicates whether this component renders itself // or delegates rendering to a renderer. public boolean getRendersSelf() { return true; } // This method, which is called during the Render Response phase, // generates the markup that represents the component. public void encodeEnd(FacesContext context) throws java.io.IOException { ResponseWriter writer = context.getResponseWriter(); HttpServletRequest request = (HttpServletRequest) context.getServletRequest(); // Represent this component as an HTML anchor element with an // image. When the image is clicked, the current page is reloaded // with a request parameter named component whose value is this // component's ID. writer.write("<a href='?component=" + getComponentId() + "'>"); writer.write("<img border='0' src='"); writer.write(request.getContextPath() + (String)getURL() + "'/>"); writer.write("</a>"); } // This method, which is called during the Apply Request Values phase, // decodes request parameters. public void decode(FacesContext context) throws java.io.IOException { HttpServletRequest request = (HttpServletRequest) context.getServletRequest(); // If there's a request parameter named component whose value // matches this component's ID... if(getComponentId().equals(request.getParameter("component"))) { // ...enqueue a command event on the FacesContext event queue // for this component. The processEvent method, listed below, // processes that event. context.addRequestEvent(this, new CommandEvent(this, clickCommand)); } } // This method, which is called during the Handle Request Events phase, // processes command events that were added to the FacesContext by // the decode method. public boolean processEvent(FacesContext context, FacesEvent event) { if(event instanceof CommandEvent) { CommandEvent cmdEvent = (CommandEvent)event; // If the event's command name equals "click"... if(clickCommand.equals(cmdEvent.getCommandName())) { // ...toggle the component's image. toggleImage(); } return false; // Go directly to render phase. } return true; // Process request normally. } // This method returns a string representing the component's type. public String getComponentType() { return "com.sabreware.components.UIToggleGraphic"; } // This private method toggles the component's image. private void toggleImage() { String imageOne = (String)getAttribute("imageOne"); String imageTwo = (String)getAttribute("imageTwo"); String currentImage = (String)getAttribute("image"); if(imageTwo.equals(currentImage)) setAttribute("image", imageOne); else setAttribute("image", imageTwo); // The setURL() method is defined in the superclass (UIGraphic). setURL((String)getAttribute("image")); }}
In Listing 13, I've rewritten the component in Listing 12.
Listing 13. /WEB-INF/classes/com/sabreware/
components/UIToggleGraphic.java
package com.sabreware.components;import javax.faces.component.UIGraphic;import com.sabreware.eventHandlers.ToggleGraphicEventHandler;public class UIToggleGraphic extends UIGraphic { // This component supports one command: click private static String clickCommand = "click"; public UIToggleGraphic() { addRequestEventHandler(new ToggleGraphicEventHandler()); } public boolean getRendersSelf() { return false; } public String getComponentType() { return "com.sabreware.components.UIToggleGraphic"; } public String getRendererType() { return "ToggleGraphicHTMLRenderer"; } public String getClickCommandName() { return clickCommand; } // This private method toggles the component's image. public void toggleImage() { String imageOne = (String)getAttribute("imageOne"); String imageTwo = (String)getAttribute("imageTwo"); String currentImage = (String)getAttribute("image"); if(imageTwo.equals(currentImage)) setAttribute("image", imageOne); else setAttribute("image", imageTwo); // The setURL method is defined in the superclass (UIGraphic) setURL((String)getAttribute("image")); }}
The component's rendering code is encapsulated in a renderer, as shown in Listing 14.
package com.sabreware.renderers;import java.util.Iterator;import java.util.Vector;import javax.servlet.http.HttpServletRequest;import javax.faces.component.AttributeDescriptor;import javax.faces.component.UIComponent;import javax.faces.component.UIGraphic;import javax.faces.context.FacesContext;import javax.faces.context.ResponseWriter;import javax.faces.event.CommandEvent;import javax.faces.event.FacesEvent;import javax.faces.render.Renderer;import com.sabreware.components.UIToggleGraphic;public class ToggleGraphicHTMLRenderer extends Renderer { // This vector's iterator is returned from the getAttributeNames methods. private Vector emptyVector = new Vector(); // This method, which is called during the Apply Request Values phase, // decodes request parameters. public void decode(FacesContext context, UIComponent component) throws java.io.IOException { HttpServletRequest request = (HttpServletRequest) context.getServletRequest(); // If there's a request parameter named component whose value // matches this component's ID... if(component.getComponentId().equals( request.getParameter("component"))) { // ...enqueue a command event on the FacesContext event queue // for this component. The processEvent method, listed below, // processes that event. context.addRequestEvent(component, new CommandEvent(component, ((UIToggleGraphic)component). getClickCommandName())); } } // This method, which is called during the Render Response phase, // generates the markup that represents the component. public void encodeEnd(FacesContext context, UIComponent component) throws java.io.IOException { UIToggleGraphic toggleGraphic = (UIToggleGraphic)component; ResponseWriter writer = context.getResponseWriter(); HttpServletRequest request = (HttpServletRequest) context.getServletRequest(); // Represent this component as an HTML anchor element with an // image. When the image is clicked, the current page is reloaded // with a request parameter named component whose value is this // component's ID. writer.write("\<a href='?component=" + component.getComponentId() + "'>"); writer.write("\<img border='0' src='"); writer.write(request.getContextPath() + (String)toggleGraphic.getURL() + "'/>"); writer.write("\</a>"); } public void encodeBegin(FacesContext context, UIComponent component) throws java.io.IOException { } public void encodeChildren(FacesContext context, UIComponent component) throws java.io.IOException { } public AttributeDescriptor getAttributeDescriptor(String componentType, String name) { return null; } public AttributeDescriptor getAttributeDescriptor(UIComponent component, String name) { return null; } public Iterator getAttributeNames(String componentType) { return emptyVector.iterator(); } public Iterator getAttributeNames(UIComponent component) { return emptyVector.iterator(); } public boolean supportsComponentType(String componentType) { return "com.sabreware.components.UIToggleGraphic".equals(componentType); } public boolean supportsComponentType(UIComponent component) { return supportsComponentType(component.getComponentType()); }}
Like Listing 12's original component, the preceding renderer implements the decode()
and encodeEnd()
methods. The latter generates HTML markup for the component. The last eight methods currently must be implemented by all renderers, whether they are relevant or not, because those methods are defined by the Renderer
interface, which all renderers must implement. Hopefully, JSF 1.0 will provide an abstract class that implements the Renderer
interface and provides sane defaults for those methods.
Listing 15 lists the component's event handler.
package com.sabreware.eventHandlers;import javax.faces.event.RequestEventHandler;import javax.faces.component.UIComponent;import javax.faces.context.FacesContext;import javax.faces.event.CommandEvent;import javax.faces.event.FacesEvent;import com.sabreware.components.UIToggleGraphic;public class ToggleGraphicEventHandler extends RequestEventHandler { public boolean processEvent(FacesContext context, UIComponent component, FacesEvent event) { if(event instanceof CommandEvent) { CommandEvent cmdEvent = (CommandEvent)event; UIToggleGraphic toggleGraphic = (UIToggleGraphic)component; // If the event's command name equals "click"... if(toggleGraphic.getClickCommandName(). equals(cmdEvent.getCommandName())) { // ...toggle the component's image toggleGraphic.toggleImage(); return false; // go directly to render phase } } return true; // process request normally }}
Like the component in Listing 12, the preceding event handler implements the processEvent()
method to handle the component's request events. If that method returns true
, the JSF lifecycle proceeds normally; if not, the JSF lifecycle proceeds directly to the Render Response phase.
JavaServer Faces is a Web application framework that defines a request lifecycle and a rich component hierarchy. The request lifecycle relieves developers from writing that code in their own Web applications, which makes those applications much easier to implement. The component hierarchy lets developers implement custom components that render different markup types. Additionally, developers can implement renderers for the JSF built-in components, so those components can also generate different markup types. JavaServer Faces is expected to have a major impact on the development of Java-based Web applications.