Many enterprise
applications require a page to be displayed in Read Mode first which shall then
be allowed to be edited and save by the user. This case typically holds for
applications showing critical data prone to errors or scenarios where the data
is read my many but edited by few.
There could be
multiple ways to do it
1.
Maintaining
two pages or two separate sections in the same page where in one renders in
Read mode and other renders in write mode. This approach is very cumbersome for
maintenance purposes because there are two controls (one read and one write) to
always keep in synch.
2.
Writing
custom renderer to control the output of Standard controls. This would detect
that the page is in Read mode or Write mode and render the page accordingly.
This is a better approach that the previous one and in synch with J2EE
specifications because it still keeps the rendering logic separate from
presentation layout.
We shall discuss
option 2 in detail and following illustrates how I had implemented option 2 in
one of my previous projects. I had augmented standard JSF with primefaces
components, hence I had written custom renderer for primefaces as well.
All the controls,
section or even the page can be put inside a custom tag which will control the
Read/Write functionality. The final usage will be like the following:
<rw:readWrite readOnlyOn="#{! myBean.showInEditMode}">
<h:inputText value="#{myBean.value}" />
<h:selectOneMenu value="#{myBean.val}">
<f:selectItems value="#{myBean.items}"
</h:selectOneMenu>
</rw:readWrite>
First lets write the
Custom tag readWrite first. Following goes inside WEB-INF/readWrite-taglib.xml file
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib 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-facelettaglibrary_2_0.xsd"
version="2.0">
<namespace>http://com.ameya.com/tags/readWrite-taglib</namespace>
<tag>
<tag-name>readWrite</tag-name>
<component>
<component-type>readWrite</component-type>
</component>
<attribute id="readOnlyOn">
<name>readOnlyOn</name>
<required>true</required>
</attribute>
</tag>
</facelet-taglib>
Following goes inside WEB-INF/web.xml
file
<context-param>
<param-name>javax.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/readWrite-taglib.xml</param-value>
</context-param>
Following is the class
for readWrite component. Create a Java file with name ReadWriteComp.java
The code first
evaluates whether the attribute readOnlyOn computes to true or false. The code
then sets the flag in a new attribute on each of its children recursively. Thus
when rendering the children with a custom renderer, we can determine whether
the control has to be rendered as ReadOnly or in Write mode.
@FacesComponent(value = "readWrite")
public class ReadWriteComp extends UIComponentBase {
private static final Logger
log = Logger.getLogger(ReadWriteComp.class);
public static final String SKIP_CHILDREN = "skipChildren";
public static final String READ_ONLY_ON = "readOnlyOn";
public static final String READ_WRITE_FAMILY = "com.ameya.ReadWrite";
@Override
public String getFamily() {
return READ_WRITE_FAMILY;
}
@Override
public boolean getRendersChildren() {
return true;
}
@Override
public void encodeBegin(FacesContext context) throws IOException {
//log.debug("Begin ReadWrite Component");
ValueExpression readOnly = null;
try {
readOnly = getValueExpression(READ_ONLY_ON);
//log.debug("readOnlyOn:" +
readOnly.getValue(context.getELContext()));
} catch
(Exception e) {
log.error("Unable to evaluate readOnlyOn
tag", e);
}
for (UIComponent childComp : getChildren()) {
markChildrenReadOnly(childComp, readOnly);
}
}
@Override
public void encodeChildren(FacesContext context) throws IOException {
//log.debug("Encode ReadWrite children");
super.encodeChildren(context);
}
public void markChildrenReadOnly(UIComponent component, ValueExpression expr) {
component.setValueExpression(READ_ONLY_ON, expr);
for (UIComponent childComp :
component.getChildren()) {
markChildrenReadOnly(childComp, expr);
}
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
//log.debug("End ReadWrite Component");
}
}
Now let’s see the custom
renderer code. The Custom Renderer should just act as delegate to the actual
JSF renderer if the component is being rendered in Write mode. However it
should just output the text instead of control in case the control is being
rendered in Read mode. Hence I have a ReadWriteHelper class which maintains a list of all the custom
renderers and the default renderers (that comes packed with
JSF/RichFaces/PrimeFaces etc). Also we have a single Renderer for all the types
called ReadWriteRenderer which then delegates the rendering control to
either the Custom Renderer or to the Default Renderers from JSF.
Following is the code
for ReadWriteRenderer class which delegates rendering control to appropriate Renderer. Thus
its job is to find the correct renderer and then delegate the control when it
gets control to encode, decode or process children etc.
public class ReadWriteRenderer extends HtmlBasicRenderer {
private static final Logger
log = Logger.getLogger(ReadWriteRenderer.class);
static {
try {
ReadWriteRendererHelper.init();
log.info("Loaded configuration for
ReadWriteRenderer");
} catch
(Exception e) {
log.error("Error loading configuration for
ReadWriteRenderer", e);
}
}
@Override
public void
decode(FacesContext context, UIComponent component) {
try {
Renderer renderer = null;
if
(isReadOnlyOn(component)) {
renderer = ReadWriteRendererHelper.getRWRenderer(component.getClass());
}
else {
renderer = ReadWriteRendererHelper.getDefaultRenderer(component.getClass());
}
renderer.decode(context, component);
} catch (Exception e) {
log.error("Error while decoding " + component.getClass() + "(id=" + component.getClientId(context) + ")", e);
}
}
@Override
public void
encodeBegin(FacesContext context, UIComponent component) throws IOException {
//log.debug("ReadWriteRenderer | Encode
Begin");
try {
Renderer renderer = null;
if
(isReadOnlyOn(component)) {
renderer = ReadWriteRendererHelper.getRWRenderer(component.getClass());
}
else {
renderer = ReadWriteRendererHelper.getDefaultRenderer(component.getClass());
}
renderer.encodeBegin(context, component);
} catch (Exception e) {
log.error("Error while encodeBegin of " + component.getClass() + "(id=" +
component.getClientId(context) + ")", e);
}
}
@Override
public void
encodeChildren(FacesContext context, UIComponent component) throws IOException {
//log.debug("ReadWriteRenderer | Encode
children");
try {
Renderer renderer = null;
if
(isReadOnlyOn(component)) {
renderer = ReadWriteRendererHelper.getRWRenderer(component.getClass());
} else {
renderer = ReadWriteRendererHelper.getDefaultRenderer(component.getClass());
}
renderer.encodeChildren(context, component);
} catch
(Exception e) {
log.error(
"Error while encodeChildren of
" + component.getClass() + "(id=" + component.getClientId(context) + ")",
e);
}
}
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
//log.debug("ReadWriteRenderer | Encode End");
try {
Renderer renderer = null;
if
(isReadOnlyOn(component)) {
renderer = ReadWriteRendererHelper.getRWRenderer(component.getClass());
} else {
renderer = ReadWriteRendererHelper.getDefaultRenderer(component.getClass());
}
renderer.encodeEnd(context, component);
} catch
(Exception e) {
log.error("Error while encodeEnd of " + component.getClass() + "(id=" + component.getClientId(context) + ")", e);
}
}
@Override
public Object getConvertedValue(FacesContext
context, UIComponent component, Object submittedValue)
throws
ConverterException {
//log.debug("ReadWriteRenderer | getConvertedValue");
Renderer renderer = null;
if (! isReadOnlyOn(component)) {
renderer = ReadWriteRendererHelper.getDefaultRenderer(component.getClass());
}
if (renderer != null) {
return
renderer.getConvertedValue(context, component, submittedValue);
}
return submittedValue;
}
protected boolean isReadOnlyOn(UIComponent component) {
if
(component.getAttributes().get(ReadWriteComp.READ_ONLY_ON) != null
&& Boolean.valueOf(component.getAttributes().get(ReadWriteComp.READ_ONLY_ON).toString())) {
return true;
}
return false;
}
}
Following is the
custom renderer for InputText
public class RWInputTextRenderer extends RWDefaultRenderer {
static final Logger log = Logger.getLogger(RWDefaultRenderer.class);
protected static final String WRAPPER_TAG_NAME = "SPAN";
protected static final String VALUE = "value";
protected static final String STYLE = "style";
protected static final String STYLE_CLASS = "styleClass";
protected static final String CLASS = "class";
protected Object getComponentValue(FacesContext context,
UIComponent component) {
return getComponentAttribute(context, component,
VALUE);
}
protected Object getComponentAttribute(FacesContext
context, UIComponent component, String attrName) {
ValueExpression expr = component.getValueExpression(attrName);
if (expr != null) {
return
expr.getValue(context.getELContext());
} else if
(component.getAttributes().containsKey(attrName)) {
return
component.getAttributes().get(attrName);
}
return null;
}
protected void startWrapperTag(FacesContext context, UIComponent component) throws IOException {
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.startElement(WRAPPER_TAG_NAME, null);
Object style = getComponentAttribute(context, component, STYLE);
if(style!=null && !StringUtils.isEmpty(style.toString())) {
responseWriter.writeAttribute(STYLE, style, null);
}
Object styleClass = getComponentAttribute(context, component, STYLE_CLASS);
if(styleClass!=null && !StringUtils.isEmpty(styleClass.toString()))
{
responseWriter.writeAttribute(CLASS, styleClass, null);
}
}
protected void endWrapperTag(FacesContext context) throws IOException {
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.endElement(WRAPPER_TAG_NAME);
}
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
ResponseWriter responseWriter = context.getResponseWriter();
Object value = null;
// Render HTML Text, Image and Messages without alteration
if (component instanceof UILeaf || component instanceof UIGraphic || component instanceof UIMessage
|| component instanceof UIMessages) {
component.encodeAll(context);
} else if (component instanceof UICommand) {
// Do not render Command (CommandButton,
CommandLink etc)
} else if (component instanceof UIInput) {
// Render only value from Input
(HTMLInput, HTMLHidden etc)
UIInput inputComp = (UIInput) component;
value = getComponentValue(context,
inputComp);
// Check if a Converter exists
if
(inputComp.getConverter() != null) {
value = inputComp.getConverter().getAsString(context, component, value);
}
}
if (value instanceof Collection) {
String str = value.toString();
if
(str.startsWith("[") && str.endsWith("]")) {
value = str.substring(1, str.length() - 1);
}
}
String strVal = value == null ? "" : String.valueOf(value);
responseWriter.write(strVal);
// log.info("ReadWriteRenderer | rendered component " +
component.getClass() + " with value " +
// String.valueOf(value));
}
@Override
public void decode(FacesContext context, UIComponent component) {
// Do Nothing
}
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
// Do Nothing
}
@Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
// Do Nothing
}
@Override
public Object getConvertedValue(FacesContext
context, UIComponent component, Object value) throws ConverterException {
// Do Nothing
return value;
}
}
Similar classes can be
written for all components which includes InputText, InputTextarea,
SelectOneMenu, AutoComplete etc.
Following goes inside
the faces-config.xml file
<!-- Render-kit for Read Write
functionality -->
<render-kit>
<renderer>
<component-family>javax.faces.Input</component-family>
<renderer-type>javax.faces.Text</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.Input</component-family>
<renderer-type>javax.faces.Textarea</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.SelectOne</component-family>
<renderer-type>javax.faces.Menu</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.SelectOne</component-family>
<renderer-type>javax.faces.Radio</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>com.ameya.custom.component</component-family>
<renderer-type>com.ameya.custom.component.radio.SelectOneRadioRenderer</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.SelectMany</component-family>
<renderer-type>javax.faces.Checkbox</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.SelectBoolean</component-family>
<renderer-type>javax.faces.Checkbox</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.Command</component-family>
<renderer-type>javax.faces.Button</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.Command</component-family>
<renderer-type>javax.faces.Link</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.InputTextRenderer</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.InputTextareaRenderer</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.AutoCompleteRenderer</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.CalendarRenderer</renderer-type>
<renderer-class>com.ameya.ui.custom.ReadWriteRenderer</renderer-class>
</renderer>
</render-kit>
No comments:
Post a Comment