Tuesday, May 13, 2014

Implementing a Read Write toggle page in JSF 2



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>


This above should give a fair idea of how to implement a Read Write toggle page in JSF 2 with ease. However if you have any questions please feel free to comment. Also you can message me to send you the archive of the code for your reference.




No comments:

Post a Comment