Extending Default JSF Renderer Functionality
Mark
Basler
Status: In Early Access
Problem Description
When designing JSF components, developers have to weigh a variety of
options. One of which, is how will the component render its
markup. If the component being developed has new functionality
that isn't closely related to other existing components then the usual
methodology would be to develop a new renderer to generate the targeted
markup. This is a straight forward approach that is detailed in
the JavaEE5 Tutorial
under
the "Creating
Custom UI Components" section.
In some cases, the component to be created adds functionality to a
component that already exists, like when AJAX functionality is
added. A
developer wouldn't want to
re-implement the base rendering behavior of the entire component.
This entry explores alternative methods to render component's markup,
utilizing the base rendering functionality that already exists.
Solution
In the cases where the component to be created adds functionality to a
component that already exists, like the AJAX FileUpload JSF
component, base renderer functionality can be utilized to render the
targets markup.
This FileUpload component was designed to add AJAX functionality to the
JSF javax.faces.component.UIForm component. Since the markup to
be rendered for the FileUpload component would be the same as the
UIForm with some default attributes set, it wouldn't make sense to code
the renderer from scratch. Given the Java developers object oriented
mentality, it would make sense to extend the base functionality,
adding only the functionality specific to the AJAX FileUpload
Component.
Since the component is to be used with JSPs then the new component will
still need a separate JSP tag handler that exposes the component's
attributes. The tag handler also links the JSF
component-type
with the renderer-type. There currently isn't a non-vendor
specific
way to extend the base tag handler's functionality, but soon tools may
be
available to help create these files automatically. Detail on how
to create the custom JSP tag library descriptor and customer tag
handler are out of the scope of this entry, but these tasks are
well documented in the JavaEE5 Tutorial,
under the "Creating
the Component Tag Handler" section.
Below we discuss the different methods that can be used to render
component's markup using base renderer functionality.
Custom Renderer calls default renderer for base functionality
Using a custom renderer to call a default component renderer can make
it very easy to add functionality that doesn't alter the way the base
component is rendered. The AJAX FileUpload Component's custom tag
adds & removes some attributes that were available in the default
JSF form
tag. The removed attributes were set to required values in the
FileUpload's custom renderer. The added attributes were set as
hidden fields to be sent when the form was submitted. Once the
appropriate
values are populated in the HtmlForm component, then the default
renderer is
retrieve through the FacesContext. The default renderer's
encodeBegin & encodeEnd methods are called, passing in the HtmlForm
component that has been updated to hold the FileUpload's specific
values. The default renderer renders the component just as it
would have if the customer tag/renderer weren't used.
Below are a code snippets that demonstrate this approach.
Simple alterations were made in the coding examples for clarity.
FileUpload Custom Tag artifacts:
The tag handler for the FileUpload tag links the base JSF component ("javax.faces.HtmlForm") with the
custom renderer ("FileUploadForm")
that is defined in the FileUpload faces-config file, along with the
component-family.
public class FileUploadTag extends UIComponentELTag {
...
public String getComponentType() {
return ("javax.faces.HtmlForm");
}
public String getRendererType() {
return ("FileUploadForm");
}
...
}
faces-config.xml
<render-kit>
<renderer>
<description>
Renderer for ajax fileupload component
</description>
<component-family>javax.faces.Form</component-family>
<renderer-type>FileUploadForm</renderer-type>
<renderer-class>com.sun.javaee.blueprints.components.ui.fileupload.FileUploadRenderer</renderer-class>
</renderer>
</render-kit>
FileUploadRenderer:
The FileUploadRenderer.encodeBegin method sets HtmlForm default
attributes, lookups up the
default renderer for the "javax.faces.Form"
component-family & renderer-type, then delegate the
rendering to the base renderer's encodeBegin method, using the modified
component. The rendering of the FileUpload component's children
are sent
to self render, so no action is necessary in the FileUploadRenderer.
The FileUploadRenderer.encodeEnd
method writes out hidden fields into the form's body then delegates
the rendering to the base renderer's encodeEnd method, using the
modified component.
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
if ((context == null) || (component == null)) {
throw new NullPointerException();
}
HtmlForm outComp = (HtmlForm)component;
// custom setup of component
outComp.setEnctype("multipart/form-data");
....
// let default renderer for form render all the basic form attributes
Renderer baseRenderer=context.getRenderKit().getRenderer("javax.faces.Form", "javax.faces.Form");
baseRenderer.encodeBegin(context, outComp);
}
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
// get properties from UIOutput that were set by the specific taghandler
String serverLocationDir=(String)component.getAttributes().get("serverLocationDir");
// add hidden field to represent location of directory on server
// if doesn't exist, defaulted in FileUploadHandler
if(serverLocationDir != null) {
writer.startElement("input", component);
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("name", id + "_" + FileUploadUtil.SERVER_LOCATION_DIR, null);
writer.writeAttribute("value", serverLocationDir, null);
writer.endElement("input");
writer.write("\n");
}
// let default renderer for form render all the basic form attributes
Renderer baseRenderer=context.getRenderKit().getRenderer("javax.faces.Form", "javax.faces.Form");
baseRenderer.encodeEnd(context, component);
}
As you can see this is a straight forward approach to customizing
functionality of existing components without having to re-implement the
default renderer's functionality. Since the FileUpload is an AJAX
component, the renderer's decode method
isn't used because the form isn't submitted through the conventional
means. The form is submitted asynchronously using Dojo, through Shale-Remoting
and consumed by the FileUploadHandler.handleFileUpload method. If
the custom component was going to decode the form submission during
the "Apply Request Values" phase, then the same method of looking up
the default renderer and delegating the call to the default renderer's
decode method would apply.
Custom Renderer uses a hidden child component and delegates
rendering to it
This method also uses a custom tag handler/renderer to decorate the
base JSF component(s) and then utilizes the default renderers to render
the makeup. This approach will work for mapping a single parent
component to a single child component, but its real strength is mapping
a single parent component to multiple child components. Once the
mapping of attributes/ValueExpressions to the appropriate child
component has be performed, each individual
child component's encodeBegin/endcodeEnd methods are called in the
desired order. The child component's are stored in the parent
component's Facet Map to be retrieved for reuse in future calls.
This approach requires that all the information that is needed to
create the child components is gathered by the Parent component's tag
definition. The tag attribute data is stored inside the base JSF
component by using the component's attribute Map and ValueExpression
Map that is inherited from the UIComponent. Using the parent's
Maps directly, side-steps the usual paradigm of convenience accessor
and
mutator methods that are usual defined in a custom JSF component.
This approach can be used by components where the component developer
doesn't want to or can't create convenience methods for additional
attributes/ValueExpressions on a custom JSF component. Depicted
below is a tag handler that uses the
UIComponent's map for storing attribute values and ValueExpressions
using this approach (in BLUE).
The current FileUpload component doesn't use the hidden child rendering
approach, but below
are the relevant code snippets demostrating its implementation:
FileUploadTag Code Snippet:
public class FileUploadTag extends javax.faces.webapp.UIComponentELTag {
private javax.el.ValueExpression serverLocationDir=null;
// handle unique properties for FileUpload form
public void setServerLocationDir(ValueExpression dir) {
serverLocationDir=dir;
}
protected void setProperties(UIComponent component) {
super.setProperties(component);
UIForm outComp outComp=(HtmlForm)component;
// pull out serverLocationDir attribute
if (serverLocationDir != null) {
if (!serverLocationDir.isLiteralText()) {
outComp.setValueExpression("serverLocationDir", serverLocationDir);
} else {
outComp.getAttributes().put("serverLocationDir", serverLocationDir.getExpressionString());
}
}
}
}
FileUploadRenderer:
In the parent's custom renderer encodeBegin method, checks are
preformed to see if the child component have already been
created. If the component doesn't exist, then a child component
of the appropriate type is
created, stored in the parent component's Facet Map and updated with
the appropriate attributes/ValueExpressions. Once the child
component is acquired, the rendering is delegated to the child
component's encodeBegin method.
When the parent's custom renderer encodeEnd method is called, the child
component(s) are retrieved from the parent's Facet Map and rendering is
delegated to the child component's encodeEnd method.
FileUploadRenderer Code Snippet:
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
HtmlForm childComp=(HtmlForm)component.getFacet(CHILD_COMPONENT_ID);
if(childComp == null) {
// create new component and store it for later retrieval
childComp=new HtmlForm();
component.getFacets().put(CHILD_COMPONENT_ID, childComp);
// loop through and copy attributes from main component to child component
Object attr=null;
for(int ii=0; ii < formAttributes.length; ii++) {
attr=component.getAttributes().get(formAttributes[ii]);
if(attr != null) {
childComp.getAttributes().put(formAttributes[ii], attr);
}
}
// copy named ValueExpression(s), if necessary
childComp
.setValueExpression("serverLocationDir",
component
.getValueExpression(
"serverLocationDir"));
// CUSTOM default the HTML enctype so the fileupload will always work properly
childComp.getAttributes().put("enctype", "multipart/form-data");
...
}
...
// Have the child component render itself
childComp.encodeBegin(context);
}
public void encodeEnd(FacesContext context, UIComponent component) throws IOException
HtmlForm childComp=(HtmlForm)component.getFacet(CHILD_COMPONENT_ID);
if(childComp != null) {
// Have the child component render its end
childComp.encodeEnd(context);
}
}
private static String formAttributes[]={"id", "prependId","rendered","accept",
"acceptcharset","dir","enctype",lang","onclick",ondblclick","onkeydown","onkeypress",
"onkeyup","onmousedown","onmousemove","onmouseout","onmouseover","onmouseup","onreset",
"onsubmit","style","styleClass","target","title","binding"};
private static final String CHILD_COMPONENT_ID="bpui_fileupload_childcomponent";
As you can see this approach is more complex, but much more
powerful.
You can utilize any number of JSF component's functionality and wrap it
in a parent component for use by others. Since this mock up of
the FileUpload is an AJAX
component, the renderer's decode method
isn't used because the form isn't submitted through the conventional
means. The form is submitted asynchronously using Dojo, through Shale-Remoting
and consumed by the FileUploadHandler.handleFileUpload method. If
the
custom component was going to decode the form submission during
the "Apply Request Values" phase, then the same method of retrieving
the child components from the parent's Facet Map and delegating the
call
to the child's decode method would apply.
Generally speaking, if the decode method's of the child
components properly transfer the model data to the parent component
the rest of the JSF functionality would remain the same,
irrelevant of this hidden child approach. There may be caveats if
the
parent component tries to implement complex behavior in the child
components (e.g. converters, validators and events), but I will leave
that for the next version of the entry.
© Sun Microsystems 2006. All of the material in The
Java BluePrints Solutions Catalog is copyright-protected
and may not be published in other works without express
written permission from Sun Microsystems.