Creating a Custom JSF 1.2 Component - With Facets, Resource Handling, Events and Listeners
Next steps - working with facets
The Shuffler component is created to dynamically (re)order its child contents. It will do so using facets. The content you want this component to shuffle is passed in two or more facets. The facets are named using string representations of integers, so for example:
<shf:shuffler styleClass="mySpecialStyle" facetOrder="reverse" id="s1" >
<f:facet name="1">
... content
</f:facet>
<f:facet name="2">
... content
</f:facet>
<f:facet name="3">
... content
</f:facet>
</shf:shuffler>
Facets are automatically supported on JSF components. The getFacets() method is available inside the Shuffler component class and will return a collection of facet UIComponents. Facets are special children for a JSF component: the framework will never render the contents of facets on its own. It is up to the component to determine when and how to render the contents of its facets. So, there is some work to do for the ShuffleRenderer class. But first we need to add support for the new facetOrder attribute. Adding an attribute means:
- adding an attribute entry in the TLD
- adding support for processing the attribute in the Tag Handler (a setter and a line of code in setProperties())
- adding the attribute in saveState() and restoreState() in the Component class
Here we go:
In the tld entry, add:
<attribute>
<name>facetOrder</name>
<required>false</required>
<deferred-value>
<type>java.lang.String</type>
</deferred-value>
</attribute>
In the tag-handler class ShufflerTag add:
private ValueExpression facetOrder;
public void setFacetOrder(ValueExpression facetOrder) {
this.facetOrder = facetOrder;
}
and in setProperties():
processProperty(component, facetOrder, Shuffler.FACETORDER_ATTRIBUTE_KEY);
Finally in the component class Shuffler , add:
public static final String FACETORDER_ATTRIBUTE_KEY = "facetOrder";
@Override
public Object saveState(FacesContext facesContext) {
Object values[] = new Object[3];
values[0] = super.saveState(facesContext);
values[1] = this.getAttributes().get(STYLECLASS_ATTRIBUTE_KEY);
values[2] = this.getAttributes().get(FACETORDER_ATTRIBUTE_KEY);
return values;
}
@Override
public void restoreState(FacesContext facesContext, Object state) {
Object values[] = (Object[])state;
super.restoreState(facesContext, values[0]);
this.getAttributes().put(STYLECLASS_ATTRIBUTE_KEY, values[1]);
this.getAttributes().put(FACETORDER_ATTRIBUTE_KEY, values[2]);
}
The Shuffler also needs to make the facets available to the renderer, in the order that is prescribed by the facetOrder attribute. This attribute supports three values: normal, reverse and random.
public List<UIComponent> getOrderedFacets(FacesContext facesContext) {
// allowable values: normal (default) and reverse
// the normal order of the facets is determined by ordering the facets by name (assuming the facetnames are string representations of integers)
// create a sorted list with the integers representing the facets
List<Integer> facetIndexValues = new ArrayList();
List<String> facetNames = new ArrayList(getFacets().keySet());
for (String facetName : facetNames) {
facetIndexValues.add(new Integer(facetName));
}
Collections.sort(facetIndexValues);
// create the list of facets corrresponding to the sorted list of facet index values
List<UIComponent> orderedFacets = new ArrayList();
for (Integer index : facetIndexValues) {
orderedFacets.add(getFacets().get(index.toString()));
}
// depending on the value for the facetOrder attribute, we may need to reorganize the orderedFacets list
String facetOrder =
(String)this.getAttributes().get(Shuffler.FACETORDER_ATTRIBUTE_KEY);
if ("reverse".equalsIgnoreCase(facetOrder)) {
Collections.reverse(orderedFacets);
} else if ("random".equalsIgnoreCase(facetOrder)) {
Collections.shuffle(orderedFacets);
} else if ("normal".equalsIgnoreCase(facetOrder)) {
// need to do nothing as with normal the order returned by getFacets() is the correct one
}
return orderedFacets;
}
The ShufflerRenderer will have to do the real work. It will retrieve the facets - in the proper order - from the Shuffler Component class and ask JSF to render them.
package nl.amis.jsf.shuffler;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;
import javax.faces.component.UIComponent;
public class ShufflerRenderer extends Renderer {
@Override
public void encodeBegin(final FacesContext facesContext,
final UIComponent component) throws IOException {
super.encodeBegin(facesContext, component);
final ResponseWriter writer = facesContext.getResponseWriter();
writer.startElement("DIV", component);
String styleClass =
(String)attributes.get(Shuffler.STYLECLASS_ATTRIBUTE_KEY);
writer.writeAttribute("class", styleClass, null);
List<UIComponent> orderedFacets = ((Shuffler)component).getOrderedFacets(facesContext);
for (UIComponent facet:orderedFacets) {
facet.encodeAll(facesContext);
}
}
@Override
public void encodeEnd(final FacesContext facesContext,
final UIComponent component) throws IOException {
final ResponseWriter writer = facesContext.getResponseWriter();
writer.endElement("DIV");
}
}
With these changes, we can now add real content to the Shuffler and have it rendered, in the order we specified - which can be random. Also note that we can use an EL expression to have the facetOrder dynamically derived:
<shf:shuffler styleClass="mySpecialStyle" <b>facetOrder="#{bean.liveFacetOrder}"</b> id="s1" >
<f:facet name="1">
... content
</f:facet>
<f:facet name="2">
... content
</f:facet>
<f:facet name="3">
... content
</f:facet>
</shf:shuffler>
Downloading Resources
The next step in our exploration of the development of custom JSF components is the addition of resources like images and JavaScript libraries. Note that in JavaServer Faces 2.0 a new facility is available, especially for this purpose. However, in our 1.2 setting we have to come up with something ourselves. That is not to say no solutions exist for JSF 1.2; almost every library comes with a form of resource handling. Then there is the Weblet framework that was introduced especially for this purpose. Another option leverages JSF itself: its capability through PhaseListeners to intercept a request, interpret the requested ViewId and optionally serve up an image or JS file in response to the request. This approach is proposed in JavaServer Faces, The Complete Reference by Ed Burns and Chris Schalk. I have slightly modified there code for my own purposes. However, the central idea clearly is theirs.
My objective is to add an image to the Shuffler component. The next step will be to allow the user to click on the image and by doing so tgrigger a re-shuffle. But that part is for later, first add the image itself.
The HTML rendered by the ShufflerRenderer needs to be extended with the IMG tag, that is easy enough. Less trivial is the value for the SRC attribute on the IMG tag.
The change in the encodeBegin method in the ShufflerRenderer:
writer.startElement("IMG", component);
writer.writeAttribute("src", imageUrl( facesContext,SHUFFLE_IMAGE), null);
writer.writeAttribute("alt", "Click to reshuffle", null);
writer.writeAttribute("width", "20px", null);
writer.endElement("IMG");
With SHUFFLE_IMAGE specified as:
private static String SHUFFLE_IMAGE = "shuffleIcon.png";
The imageUrl() method is defined as follows
private final static String IMAGE_PATH ="/faces/images/";
protected String imageUrl(FacesContext facesContext, String image) {
ViewHandler handler = facesContext.getApplication().getViewHandler();
String imageUrl =
handler.getResourceURL(facesContext, IMAGE_PATH + image);
return imageUrl;
}
The URLs for images are now constructed to look like this:
http://somehost:7101/CustomJSFConsumer/faces/images/shuffleIcon.png
The request for the shuffleIcon.png that is sent by the browser should be intercepted by a component that knows how to handle it. Because of the /faces/ part, this request is sent to the FacesServlet and processed through the JSF lifecycle. The componoent to intercept it will be a phaseListener that fires after restore view. It inspects the ViewId. When the ViewId contains the predefined indicator ("/images/") it steps in and takes over processing of the request. It will find the name of the image that is requested by taking the part of the ViewId that comes after /images/. It will then locate the image file on the classpath (that works well for a component packaged in a jar file, it can have the images packaged in the jar file too), looking for a directory called /images/ - as specified by the IMAGE_PATH constant. It copies the image from the file to the outputstream after setting the content type.
package nl.amis.jsf;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
public class ResourceServerPhaseListener implements PhaseListener {
public ResourceServerPhaseListener() {
super();
}
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
public void afterPhase(PhaseEvent event) {
// If this is restoreView phase
if (PhaseId.RESTORE_VIEW == event.getPhaseId()) {
if (-1 !=
event.getFacesContext().getViewRoot().getViewId().indexOf(RENDER_IMAGE_TAG)) {
// extract the name of the image resource from the ViewId
String image =
event.getFacesContext().getViewRoot().getViewId().substring(event.getFacesContext()
.getViewRoot().getViewId().indexOf(RENDER_IMAGE_TAG) +
RENDER_IMAGE_TAG.length());
// render the script
writeImage(event, image);
event.getFacesContext().responseComplete();
}
}
}
public void beforePhase(PhaseEvent event) {
}
public static final String RENDER_IMAGE_TAG = "/images/";
public static final String IMAGE_PATH = "/images/";
private void writeImage(PhaseEvent event, String resourceName) {
URL url = getClass().getResource(IMAGE_PATH + resourceName);
URLConnection conn = null;
InputStream stream = null;
HttpServletResponse response =
(HttpServletResponse)event.getFacesContext().getExternalContext().getResponse();
try {
conn = url.openConnection();
conn.setUseCaches(false);
stream = conn.getInputStream();
ServletContext servletContext =
(ServletContext)FacesContext.getCurrentInstance().getExternalContext().getContext();
String mimeType = servletContext.getMimeType(resourceName);
response.setContentType(mimeType);
response.setStatus(200);
// Copy the contents of the file to the output stream
byte[] buf = new byte[1024];
int count = 0;
while ((count = stream.read(buf)) >= 0) {
response.getOutputStream().write(buf, 0, count);
}
response.getOutputStream().close();
} catch (Exception e) {
String message = null;
message = "Can't load image file:" + url.toExternalForm();
try {
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
message);
} catch (IOException f) {
f.printStackTrace();
}
}
}
}
PhaseListeners need to be configured in order to be active. This configuration usually is done in the faces-config.xml of the application. Fortunately, we can also configure the PhaseListener in the faces-config.xml file that we create for the custom component. This faces-config.xml is part of the jar file in which the custom component is shipped and deployed. Its contents are merged with the application’s own faces-config.xml. The registration of our PhaseListener looks like this:
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee">
<lifecycle>
<phase-listener>nl.amis.jsf.ResourceServerPhaseListener</phase-listener>
</lifecycle>
<component>
...
Triggering events on the custom component
Time to take another big step. We will support clicking the image by the end user and turn that event into a reshuffle of the facets of the Shuffler component. In the next section we will not only act on that click ourselves, but also publish an event that others can listen to.
We will have to add a JavaScript event listener in the HTML rendered for the Shuffler. This client side code is triggered when the image is clicked. It will submit the form - after it has added an input element to the DOM and set a value on it. Note: this approach to have a custom component trigger an event that can be received by the server side renderer class has been described in Pro JSF and Ajax: Building Rich Internet Components - by John R. Fallows and Jonas Jacobi, the guys who first introduced me to JavaServer Faces.
The JavaScript for the Shuffler component looks like this:
/**
* The onclick handler for ShufflerRenderer.
*
* @param formClientId the clientId of the enclosing UIForm component
* @param clientId the clientId of the Shuffler component
*/
function _shuffle_click( formClientId, clientId)
{
var form = document.forms[formClientId];
var input = form[clientId];
if (!input) // if the input element does not already exist, create it and add it to the form
{
input = document.createElement("input");
input.type = 'hidden';
input.name = clientId;
form.appendChild(input);
}
input.value = 'clicked';
form.submit();
}
The JavaScript is not be directly included in the page - as it is part of the jar file in which the Shuffler component is shipped. We need a way to attach this JavaScript (it is in a file called shuffle.js) to the page from within the custom component, or in this case rather its Renderer class. We extend the ResourceServerPhaseListener to also handle JavaScript resources, just like it can handle images.
public class ResourceServerPhaseListener implements PhaseListener {
public static final String RENDER_SCRIPT_TAG = "/js/";
public static final String RENDER_IMAGE_TAG = "/images/";
public static final String SCRIPT_PATH = "/js/";
public static final String IMAGE_PATH = "/images/";
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
public void afterPhase(PhaseEvent event) {
// If this is restoreView phase
if (PhaseId.RESTORE_VIEW == event.getPhaseId()) {
// if the request is for a JavaScript library
if (-1 != event.getFacesContext().getViewRoot().getViewId().indexOf(RENDER_SCRIPT_TAG)) {
// extract the name of the script from the ViewId
String script =
event.getFacesContext().getViewRoot().getViewId().substring(event.getFacesContext()
.getViewRoot().getViewId().indexOf(RENDER_SCRIPT_TAG) +
RENDER_SCRIPT_TAG.length());
// render the script
writeScript(event, script);
event.getFacesContext().responseComplete();
}
... image handling, same as before
}
}
public void beforePhase(PhaseEvent event) {
}
private void writeScript(PhaseEvent event, String resourceName) {
URL url = getClass().getResource(SCRIPT_PATH + resourceName);
URLConnection conn = null;
InputStream stream = null;
BufferedReader bufReader = null;
HttpServletResponse response =
(HttpServletResponse)event.getFacesContext().getExternalContext().getResponse();
OutputStreamWriter outWriter = null;
String curLine = null;
try {
outWriter =
new OutputStreamWriter(response.getOutputStream(), response.getCharacterEncoding());
conn = url.openConnection();
conn.setUseCaches(false);
stream = conn.getInputStream();
bufReader = new BufferedReader(new InputStreamReader(stream));
response.setContentType("text/javascript");
response.setStatus(200);
while (null != (curLine = bufReader.readLine())) {
outWriter.write(curLine + "\n");
}
outWriter.close();
} catch (Exception e) {
String message = null;
message = "Can't load script file:" + url.toExternalForm();
try {
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
message);
} catch (IOException f) {
f.printStackTrace();
}
}
}
private void writeImage(PhaseEvent event, String resourceName) {
... same as before
}
}
The Renderer class is responsible for rendering the markup that will include the JavaScript resources to the page (the script element). We could have multiple occurrences of our custom component in a page. However, the JavaScript file shuffle.js should be loaded only once, to prevent excessive and completely pointless browser requests. In order to make that happen, the Renderer indicates to a method writeScriptResource that it has a JavaScript resource that should be included. This method verifies whether a script tag for downloading that same resource has already been added in the current request. If so, it will not add another script tag. If not [already included] then the tag is added with its src attribute referring to the proper PhaseListener controlled url:
protected void writeScriptResource(
FacesContext context,
String resourcePath) throws IOException
{
Set scriptResources = _getScriptResourcesAlreadyWritten(context);
// Set.add() returns true only if item was added to the set
// and returns false if item was already present in the set
if (scriptResources.add(resourcePath))
{
ViewHandler handler = context.getApplication().getViewHandler();
String resourceURL = handler.getResourceURL(context, SCRIPT_PATH +resourcePath);
ResponseWriter out = context.getResponseWriter();
out.startElement("script", null);
out.writeAttribute("type", "text/javascript", null);
out.writeAttribute("src", resourceURL, null);
out.endElement("script");
}
}
private Set _getScriptResourcesAlreadyWritten(
FacesContext context)
{
ExternalContext external = context.getExternalContext();
Map requestScope = external.getRequestMap();
Set written = (Set)requestScope.get(_SCRIPT_RESOURCES_KEY);
if (written == null)
{
written = new HashSet();
requestScope.put(_SCRIPT_RESOURCES_KEY, written);
}
return written;
}
static private final String _SCRIPT_RESOURCES_KEY =
ShufflerRenderer.class.getName() + ".SCRIPTS_WRITTEN";
With these helper methods in place, the ShufflerRenderer can be extended to include the client side click handling code:
@Override
public void encodeBegin(final FacesContext facesContext,
final UIComponent component) throws IOException {
super.encodeBegin(facesContext, component);
final Map<String, Object> attributes = component.getAttributes();
final ResponseWriter writer = facesContext.getResponseWriter();
String formClientId = _findFormClientId(facesContext, component);
String shuffleClientId = component.getClientId(facesContext);
<b>writeScriptResource(context, "shuffle.js");
</b>writer.startElement("DIV", component);
String styleClass =
(String)attributes.get(Shuffler.STYLECLASS_ATTRIBUTE_KEY);
writer.writeAttribute("class", styleClass, null);
<b>writer.startElement("SPAN", component);
writer.writeAttribute("onClick",
"_shuffle_click('" + formClientId +
"'," + "'" + shuffleClientId + "')", null);</b>
writer.startElement("IMG", component);
writer.writeAttribute("src", imageUrl( facesContext,SHUFFLE_IMAGE), null);
writer.writeAttribute("alt", "Click to reshuffle", null);
writer.writeAttribute("width", "20px", null);
writer.endElement("IMG");
<b> writer.endElement("SPAN");</b>
List<UIComponent> orderedFacets = ((Shuffler)component).getOrderedFacets(facesContext);
for (UIComponent facet:orderedFacets) {
facet.encodeAll(facesContext);
}
}
protected void encodeResources(FacesContext context,
UIComponent component) throws IOException {
writeScriptResource(context, "shuffle.js");
}
/**
* Finds the parent UIForm component client identifier.
*
* @param context the Faces context
* @param component the Faces component
*
* @return the parent UIForm or RichForm (for usage in ADF) client identifier, if present, otherwise null
*/
private String _findFormClientId(FacesContext context,
UIComponent component) {
if (component==null) {
return null;
}
if (component instanceof UIForm || component.getClass().getName().endsWith("RichForm")) {
return component.getClientId(context);
}
else {
return _findFormClientId(context, component.getParent());
}
}
The image is wrapped in a SPAN and the onclick event handler is defined on that SPAN element (this allows us to later on add more clickable stuff to the SPAN). When the image is clicked, the _shuffle_click function is invoked - that was loaded from shuffle.js. The element is added to the form and the form is submitted.
The HTML rendered from this renderer now looks like this:
<script src="/CustomJSFConsumer-ViewController-context-root/faces/js/shuffle.js" type="text/javascript">
<div class="h1">
<span onclick="_shuffle_click('f1','s1')">
<img width="20" alt="Click to reshuffle" src="/CustomJSFConsumer-ViewController-context-root/faces/images/shuffleIcon.png"/>
</span>
... content from the facets
</div>
When the user clicks on the image, the following request is sent to the server:
| javax.faces.ViewState | !-osretn5tu |
| org.apache.myfaces.trinidad.faces.FORM | f1 |
| s1 | clicked |
This request is processed through the JSF lifecycle. The ShufflerRenderer should implement the decode() method to determine whether the s1 element is in the request, with the value clicked. If it finds that this is the case, it has established that the user has indeed clicked on the shuffle image, and appropriate action should take place. Here is the code for that decode() implementation:
@Override
public void decode(FacesContext facesContext, UIComponent component) {
ExternalContext external = facesContext.getExternalContext();
Map requestParams = external.getRequestParameterMap();
String clientId = component.getClientId(facesContext);
String clicked = (String)requestParams.get(clientId);
if (clicked != null && clicked.length() > 0 &&
"clicked".equalsIgnoreCase(clicked)) {
((Shuffler)component).notifyOfShuffleEvent(facesContext);
} //if
} //decode
This code calls upon the Shuffler component to handle the event. The notifyOfShuffleEvent() method in the Shuffler component is implemented (for now at least) in simple way:
void notifyOfShuffleEvent(FacesContext facesContext) {
String facetOrder =
(String)getAttributes().get(Shuffler.FACETORDER_ATTRIBUTE_KEY);
facetOrder = "reverse".equalsIgnoreCase(facetOrder)?"normal":
("normal".equalsIgnoreCase(facetOrder)?"reverse":facetOrder);
getAttributes().put(Shuffler.FACETORDER_ATTRIBUTE_KEY, facetOrder);
}
Later on we will have this method instantiate and broadcast a FacesEvent and invoke a directly registered listener.
When the page is rerendered, the facetOrder of the component has been swapped from normal to reverse or vice versa or it has stayed at random. In all cases, rerendering the page will have the effect of reshuffling the contents of the Shuffler component.
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)





Comments
Jacek Furmankiewicz replied on Wed, 2009/10/07 - 12:25pm
Don't get me wrong, it's a nice article...but I think I'd rather just use jQuery UI which can do custom components using a plugin and 1 line of Javascript.
I think instead of looking at these heavy server-side frameworks Java programmers should look at jQuery UI instead (which makes modern Javascript easy) and just expose data from the server-side Java apps via REST or something like that.
Here's the jquery code to turn an order list of divs into a proper accordion component
http://jqueryui.com/demos/accordion/
Compared to that, the JSF 1.2 approach looks insanely compilicated.
Nicolas Frankel replied on Thu, 2009/10/08 - 3:00am
in response to:
Jacek Furmankiewicz
Jacek,
Executing code client-side or server-side is a major architectural decision that you don't have the luxury to make! For most clients I have worked for, these choices were made before I came. It has nothing to do with this article whatsoever.
Besides, you'll get the same problem in developing from scratch for whatever framework you're using (Swing, .Net, what have you) if you don't have the component you want when needed, which is a common occurence. Making reusable components libraries for your organization is a big step in speeding up development.
I must concede though that JSF architecture favors ease of use over ease of development so new components are a bit complex to develop.
Michael Mosmann replied on Fri, 2009/10/09 - 2:54am
in response to:
Nicolas Frankel
Hi,
> Executing code client-side or server-side is a major architectural decision that you don't have the luxury to make!
And if you have the luxury sometimes you should not...
> Besides, you'll get the same problem in developing from scratch for whatever framework you're using (Swing, .Net, what have you) if you don't have the component you want when needed, which is a common occurence.
Sure.. but: JSF makes this too heavy..
I wonder if anyone has fun in doing component development with JSF. It's a pain and i see no chance that JSF2 can reduce this much. I do not know any UI-Framework which makes this more painfull.
> Making reusable components libraries for your organization is a big step in speeding up development.
Then anybody should use a framework which has its strength in this particular aspect.Why should anybody use JSF? I don't get it.
> I must concede though that JSF architecture favors ease of use over ease of development so new components are a bit complex to develop.
There are ways out of this.. wicket, gwt, vaadin.
Bill Bryson replied on Sat, 2009/10/10 - 11:12am
A good well written article but also a reminder why I'm never going to willingly go near a JSF project again.
Jose Maria Arranz replied on Mon, 2009/10/12 - 4:53am
@nfrankel: Besides, you'll get the same problem in developing from scratch for whatever framework you're using (Swing, .Net, what have you) if you don't have the component you want when needed, which is a common occurence.
Really?