In this recipe, we get to the next level and we will create a much complex custom component. Step by step, we will build an image slide viewer with AJAX functionality.
Remember that we will consider the ideas from the previous two recipes to be already known, therefore it is mandatory to read them first!
We have developed this recipe with NetBeans 6.8, JSF 2.0, and GlassFish v3. The JSF 2.0 classes were obtained from the NetBeans JSF 2.0 bundled library. In addition, we have used the Dynamic Faces project, which provides support for JSF 2.0 and extends the JSF lifecycle to work on AJAX requests. You can download this distribution from https://jsf-extensions.dev.java.net/. The Dynamic Faces libraries (including necessary dependencies) are in the book code bundle, under the /JSF_libs/Dynamic Faces JSF 2.0
folder.
Our recipe will have three stages. In the first stage, our component will be a simple image viewer. In the next stage, it will be an image slide viewer, and in the final stage, it will become an image slide viewer with AJAX functionality.
To begin with we develop the component class. This time we render an image to the client, therefore our component will extend the UIOutput
component, as shown next (the picture is characterized by three attributes—width
(image width), height
(image height), and path
(image URL)):
package custom.component; import javax.faces.component.UIOutput; public class UIImageOutput extends UIOutput { private static final String IMAGE_FAMILY = "IMAGE_FAMILY"; private String width; private String height; private String path; public String getHeight() { return height; } public void setHeight(String height) { this.height = height; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getWidth() { return width; } public void setWidth(String width) { this.width = width; } public UIImageOutput() { super(); } @Override public String getFamily() { return IMAGE_FAMILY; } }
Next, we implement the tag handler class. There is nothing special to it, therefore we can write it right away:
package custom.component; import javax.faces.component.UIComponent; import javax.faces.webapp.UIComponentELTag; public class UIImageOutputTag extends UIComponentELTag { private static final String IMAGE_OUTPUT = "IMAGE_OUTPUT"; private static final String IMAGE_RENDERER = "IMAGE_RENDERER"; private String width; private String height; private String path; public String getHeight() { return height; } public void setHeight(String height) { this.height = height; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getWidth() { return width; } public void setWidth(String width) { this.width = width; } public String getComponentType() { return IMAGE_OUTPUT; } public String getRendererType() { return IMAGE_RENDERER; } @Override protected void setProperties(UIComponent ui_comp) { super.setProperties(ui_comp); UIImageOutput uiImageOutput = (UIImageOutput)ui_comp; if (path != null) { uiImageOutput.setPath(path); } if (width != null) { uiImageOutput.setWidth(width); } if (height != null) { uiImageOutput.setHeight(height); } } }
Finally, we must create a custom renderer for our component. Obviously, we need only the encodeBegin
method, therefore our job becomes easy:
package custom.component; import java.io.IOException; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.Renderer; import javax.servlet.ServletContext; public class UIImageOutputRenderer extends Renderer{ @Override public void encodeBegin(FacesContext ctx, UIComponent ui_comp) throws IOException { UIImageOutput uiImageOutput = (UIImageOutput)ui_comp; ResponseWriter responseWriter = ctx.getResponseWriter(); responseWriter.startElement("div",ui_comp); String width = uiImageOutput.getWidth(); String height = uiImageOutput.getHeight(); ServletContext servletContext = (ServletContext) ctx.getExternalContext().getContext(); String contextPath = servletContext.getContextPath(); responseWriter.startElement("img", uiImageOutput); responseWriter.writeAttribute("src", contextPath + uiImageOutput.getPath(), "path"); responseWriter.writeAttribute("width", width, "width"); responseWriter.writeAttribute("height", height, "height"); responseWriter.endElement("div"); } }
At the configuration level, we need to add the component and the renderer in the faces-config.xml
file:
<?xml version='1.0' encoding='UTF-8'?> <faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"> <component> <component-type>IMAGE_OUTPUT</component-type> <component-class>custom.component.UIImageOutput</component-class> </component> <render-kit> <renderer> <description> Renderer for the image component. </description> <component-family>IMAGE_FAMILY</component-family> <renderer-type>IMAGE_RENDERER</renderer-type> <renderer-class> custom.component.UIImageOutputRenderer </renderer-class> </renderer> </render-kit> </faces-config>
Now, it is time to test our component, and for this we wrote the following view (JSP page):
<%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%> <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%> <%@taglib prefix="e" uri="http://packt.net/cookbook/components"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <f:view> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title>JSF image viewer custom component</title> </head> <body> <h3><h:outputText value="This image is provided by a JSF custom component:"/></h3> <h:form> <e:imgOutput path="/img/rafa_1.jpg" width="340" height="466" /> </h:form> </body> </html> </f:view>
The output is shown next:
We continue to extend our previous component to become an image slide viewer. In the end, the component will display one image at a time, and will have two buttons for navigating to the next/previous image. The images will be specified in the path
attribute separated by a comma, as shown next:
<e:imgOutput path="/img/rafa_1.jpg, /img/rafa_2.jpg, /img/rafa_3.jpg, /img/rafa_4.jpg, /img/rafa_5.jpg" width="340" height="466" />
Now, let's see the modifications that we should accomplish. To begin with, we modify the component class by adding two more properties, one for holding the image count (we name it imgIndex
) and one for storing image URLs (we name it paths
). In addition, in this class, we will override two more methods—saveState
and restoreState
. These methods are responsible for preserving the state of the component. Now, the component class is:
package custom.component; import javax.faces.component.UIOutput; import javax.faces.context.FacesContext; public class UIImageOutput extends UIOutput { private static final String IMAGE_FAMILY = "IMAGE_FAMILY"; private String width; private String height; private String path; private String[] paths; private int imgIndex; public int getImgIndex() { return imgIndex; } public void setImgIndex(int imgIndex) { this.imgIndex = imgIndex; } public String[] getPaths() { return paths; } public void setPaths(String[] paths) { this.paths = paths; } public String getHeight() { return height; } public void setHeight(String height) { this.height = height; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getWidth() { return width; } public void setWidth(String width) { this.width = width; } public UIImageOutput() { super(); } @Override public Object saveState(FacesContext cxt) { Object state[] = new Object[5]; state[0] = super.saveState(cxt); state[1] = paths; state[2] = new Integer(imgIndex); state[3] = width; state[4] = height; return state; } @Override public void restoreState(FacesContext cxt, Object obj) { Object state[] = (Object[])obj; super.restoreState(cxt,state[0]); paths = (String[])state[1]; imgIndex = ((Integer)state[2]).intValue(); width = (String)state[3]; height = (String)state[4]; } @Override public String getFamily() { return IMAGE_FAMILY; } }
Next, we add a minor but significant modification to the tag handler class. The idea is to split the path
attribute content, using the comma delimiter, to extract the images paths. Here is the new setProperties
method:
… @Override protected void setProperties(UIComponent ui_comp) { super.setProperties(ui_comp); UIImageOutput uiImageOutput = (UIImageOutput)ui_comp; if (path != null) { String[] imgPaths = path.trim().split(","); uiImageOutput.setPath(imgPaths[0]); uiImageOutput.setPaths(imgPaths); } if (width != null) { uiImageOutput.setWidth(width); } if (height != null) { uiImageOutput.setHeight(height); } } …
The last modification is also the most consistent one. We adapt the component renderer for rendering HTML and JavaScript. When the client presses the navigation buttons, the component should trigger the onClick
mouse event. The JavaScript associated with the onClick
mouse event submits the form. In addition, we need a hidden field to hold the information provided by the JavaScript about the clicked button. A JavaScript snippet is shown next (this is copied from browser's page source):
<script type="text/javascript"> var j_id_id28j_id_id30_F = document.forms['j_id_id28']; function j_id_id30_PB(element) { if (j_id_id28j_id_id30_F.onsubmit == null || j_id_id28j_id_id30_F.onsubmit()) { j_id_id28j_id_id30_F.j_id_id28_j_id_id30_H.value = element.id; j_id_id28j_id_id30_F.submit(); } } </script>
For implementing this we need four methods as follows:
private UIForm getUIForm(UIComponent ui_comp) { UIComponent uiParent = ui_comp.getParent(); if (uiParent == null) { throw new IllegalStateException("Form unavailable!"); } while (uiParent != null) { if (uiParent instanceof UIForm) { break; } uiParent = uiParent.getParent(); } return (UIForm) uiParent; } private String previousLink(FacesContext ctx, UIComponent ui_comp){ String clientId = getUIForm(ui_comp).getId(); String uiClientId = ui_comp.getId(); String result = clientId + "_" + uiClientId + "_P"; return result; } private String nextLink(FacesContext ctx, UIComponent ui_comp){ String clientId = getUIForm(ui_comp).getId(); String uiClientId = ui_comp.getId(); String result = clientId + "_" + uiClientId + "_N"; return result; } private String hiddenField(FacesContext ctx,UIComponent ui_comp){ String clientId = getUIForm(ui_comp).getId(); String uiClientId = ui_comp.getId(); String result = clientId + "_" + uiClientId + "_H"; return result; }
Finally, we need to modify the encodeBegin
method and implement the decode
method, as shown next (the decode
method will take care the index value of paths
relative to the hidden field value and set the path
property based on the index value):
package custom.component; import java.io.IOException; import java.util.Map; import javax.faces.component.UIComponent; import javax.faces.component.UIForm; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.Renderer; import javax.servlet.ServletContext; public class UIImageOutputRenderer extends Renderer{ private UIForm getUIForm(UIComponent ui_comp) { UIComponent uiParent = ui_comp.getParent(); if (uiParent == null) { throw new IllegalStateException("Form unavailable!"); } while (uiParent != null) { if (uiParent instanceof UIForm) { break; } uiParent = uiParent.getParent(); } return (UIForm) uiParent; } private String previousLink(FacesContext ctx, UIComponent ui_comp){ String clientId = getUIForm(ui_comp).getId(); String uiClientId = ui_comp.getId(); String result = clientId + "_" + uiClientId + "_P"; return result; } private String nextLink(FacesContext ctx, UIComponent ui_comp){ String clientId = getUIForm(ui_comp).getId(); String uiClientId = ui_comp.getId(); String result = clientId + "_" + uiClientId + "_N"; return result; } private String hiddenField(FacesContext ctx, UIComponent ui_comp){ String clientId = getUIForm(ui_comp).getId(); String uiClientId = ui_comp.getId(); String result = clientId + "_" + uiClientId + "_H"; return result; } @Override public void encodeBegin(FacesContext ctx, UIComponent ui_comp) throws IOException { UIImageOutput uiImageOutput = (UIImageOutput)ui_comp; ResponseWriter responseWriter = ctx.getResponseWriter(); responseWriter.startElement("table", uiImageOutput); // get "id" attribute String id = (String)uiImageOutput.getClientId(ctx); responseWriter.writeAttribute("id", id, null); //Java Script postback code UIForm uiForm = getUIForm(uiImageOutput); String clientId = uiForm.getClientId(ctx); String postBack = uiImageOutput.getId() + "_PB"; String formName = uiForm.getId() + uiImageOutput.getId() + "_F"; responseWriter.startElement("script", uiImageOutput); responseWriter.writeAttribute("type", "text/javascript", null); String script = " var " + formName + " = document.forms['" + clientId + "'];" + " function" + " " + postBack + "(element) { " + " if (" + formName + ".onsubmit == null || " + formName + ".onsubmit()) { " + " " + formName + "." + hiddenField(ctx, uiImageOutput) + ".value = element.id; " + " " + formName + ".submit();" + " } } "; responseWriter.writeText(script, ui_comp, null); responseWriter.endElement("script"); responseWriter.startElement("input", uiImageOutput); responseWriter.writeAttribute("type", "hidden", null); responseWriter.writeAttribute("name", hiddenField(ctx, uiImageOutput), null); responseWriter.writeAttribute("value", "", null); responseWriter.endElement("input"); // "tr" element responseWriter.startElement("tr", uiImageOutput); // "td" element (image) responseWriter.startElement("td", uiImageOutput); // Render the image ServletContext servletContext = (ServletContext)ctx. getExternalContext().getContext(); String contextPath = servletContext.getContextPath(); responseWriter.startElement("img", uiImageOutput); responseWriter.writeAttribute("src", contextPath + uiImageOutput.getPath(), "url"); responseWriter.writeAttribute("width", uiImageOutput.getWidth(), "width"); responseWriter.writeAttribute("height", uiImageOutput.getHeight(), "height"); responseWriter.endElement("td"); responseWriter.endElement("tr"); // "tr" element responseWriter.startElement("tr", uiImageOutput); // "td" element (links) responseWriter.startElement("td", uiImageOutput); // Previous image link responseWriter.startElement("input", uiImageOutput); responseWriter.writeAttribute("type", "button" , null); responseWriter.writeAttribute("value", "Previous" , null); responseWriter.writeAttribute("onClick", "javascript:" + postBack + "(this)", null); responseWriter.writeAttribute("id", previousLink(ctx, ui_comp), null); responseWriter.endElement("input"); // Next image link responseWriter.startElement("input", uiImageOutput); responseWriter.writeAttribute("type", "button" , null); responseWriter.writeAttribute("value", "Next" , null); responseWriter.writeAttribute("onClick", "javascript:" + postBack + "(this)", null); responseWriter.writeAttribute("id", nextLink(ctx, ui_comp), null); responseWriter.endElement("input"); responseWriter.endElement("td"); responseWriter.endElement("tr"); responseWriter.endElement("table"); } @Override public void decode(FacesContext ctx, UIComponent ui_comp) { if ((ctx == null) || (ui_comp == null)) { throw new NullPointerException(); } UIImageOutput uiImageOutput = (UIImageOutput)ui_comp; String hidden_field = hiddenField(ctx, uiImageOutput); Map paramsMap = ctx.getExternalContext(). getRequestParameterMap(); String valH = (String)paramsMap.get(hidden_field); String[] img_paths = uiImageOutput.getPaths(); int img_index = uiImageOutput.getImgIndex(); if (valH.equals(previousLink(ctx, ui_comp))){ if (img_index > 0){ img_index = img_index-1; uiImageOutput.setImgIndex(img_index); } }else if (valH.equals(nextLink(ctx, ui_comp))){ if (img_index < img_paths.length - 1){ img_index = img_index+1; uiImageOutput.setImgIndex(img_index); } } uiImageOutput.setPath(img_paths[img_index]); } }
Finally, we modify the JSP page that uses our component as shown next:
<e:imgOutput path="/img/rafa_1.jpg, /img/rafa_2.jpg, /img/rafa_3.jpg, /img/rafa_4.jpg, /img/rafa_5.jpg" width="340" height="466" />
Now, you can test the application again!
We continue by adding AJAX capabilities to our image slide viewer. For this, we will use the Dynamic Faces project, which extends the JSF lifecycle to work on AJAX requests. After you have downloaded Dynamic Faces from https://jsf-extensions.dev.java.net/ and placed the libraries in your project, you must accomplish a set of modifications to enable AJAX on this custom component.
We start with a configuration task that should be accomplished in the web.xml
descriptor. Add the following lines to the Faces Servlet:
<servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <!-- For Dynamic Faces --> <init-param> <param-name>javax.faces.LIFECYCLE_ID</param-name> <param-value>com.sun.faces.lifecycle.PARTIAL</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
Next, modify the encodeBegin
method of the renderer class, as shown next:
@Override public void encodeBegin(FacesContext ctx, UIComponent ui_comp) throws IOException { UIImageOutput uiImageOutput = (UIImageOutput)ui_comp; ResponseWriter responseWriter = ctx.getResponseWriter(); responseWriter.startElement("table", uiImageOutput); // get "id" attribute String id = (String)uiImageOutput.getClientId(ctx); responseWriter.writeAttribute("id", id, null); //Java Script postback code UIForm uiForm = getUIForm(uiImageOutput); String clientId = uiForm.getClientId(ctx); String postBack = uiImageOutput.getId() + "_PB"; String formName = uiForm.getId() + uiImageOutput.getId() + "_F"; responseWriter.startElement("script", uiImageOutput); responseWriter.writeAttribute("type", "text/javascript", null); //with AJAX String script = " var " + formName + " = document.forms['" + clientId + "'];" + " function" + " " + postBack + "(element) { " + " if (" + formName + ".onsubmit == null || " + formName + ".onsubmit()) { " + " document.getElementById('" + hiddenField(ctx, uiImageOutput) + "').value = element.id; " + " DynaFaces.fireAjaxTransaction(element,{execute:'" + id + "',render:'" + id + "',inputs:'" + hiddenField(ctx, uiImageOutput) + "'});" + " } } "; responseWriter.writeText(script, ui_comp, null); responseWriter.endElement("script"); responseWriter.startElement("input", uiImageOutput); responseWriter.writeAttribute("type", "hidden", null); //with AJAX responseWriter.writeAttribute("id", hiddenField(ctx, iImageOutput), null); responseWriter.writeAttribute("value", "", null); responseWriter.endElement("input"); …
Finally, modify the JSP page to add Dynamic Faces taglib
, and add the <jsfExt:scripts>
tag to the <head>
, as shown next:
<%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%> <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%> <%@taglib prefix="e" uri="http://packt.net/cookbook/components"%> <%@taglib prefix="jsfxt" uri="http://java.sun.com/jsf/extensions/dynafaces"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <f:view> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title>JSF custom component, AJAX enabled</title> <jsfxt:scripts /> </head> <body> <h3><h:outputText value="This images are provided by a JSF custom component AJAX enabled:"/></h3> <h:form> <e:imgOutput path="/img/rafa_1.jpg, /img/rafa_2.jpg, /img/rafa_3.jpg, /img/rafa_4.jpg, /img/rafa_5.jpg" width="340" height="466" /> </h:form> </body> </html> </f:view>
Test the application again, and notice how AJAX is getting into the equation!