Pages

Friday, September 23, 2011

Creating a JSF2 Console Component

Since most of the people coming to my site are still looking for JSF how-to's here is one more. This is an how-to on creating a console component, like the one here, with just the JSF2 api.
A shell like console's basic requirements look like this ;
  • Should prompt the user to enter a command. 
  • Evaluate the command and print the result.
  • Should limit the text to be entered only on the prompt line limiting the movement of the caret. 
  • The new command line should be empty for user to enter a new command.
  • Should support AJAX
Since all a JSF component does is rendering HTML and managing state we should actually find a way to implement this with HTML and JavaScript. 
Here are two options I could think of ;
  • Render a textarea, try to limit the movement of the caret through JavaScript.
  • Render a input text for the user command. Print outputs within a 'div'. Blend them together using CSS.
I chose the second approach since JavaScript needed for he first approach is a bit tricky to implement.
Now to implement these as a JSF component we need these :
  • A taglib defining our component.
  • A State manager component that manages inputs and outputs of the component and beans.
  • A Renderer that will output the html depending on components state
The taglib is actually quite simple :
 <?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://mca.org/qmass</namespace>  
   <tag>  
     <tag-name>console</tag-name>  
     <component>  
       <component-type>UIConsole</component-type>  
       <renderer-type>UIConsole</renderer-type>  
     </component>  
   </tag>  
 </facelet-taglib>  
We will refence this taglib through our xhtml's using namespace and our component tag. Component-type references to what I called state-manager and renderer-type references to the renderer.
The state manager :
 @FacesComponent(value = "UIConsole")  
 public class UIConsole extends UICommand {  
   public String getCommand() {  
     return (String) getStateHelper().eval("command");  
   }  
   public void setCommand(String command) {  
     getStateHelper().put("command", command);  
     getValueExpression("command").setValue(FacesContext.getCurrentInstance().getELContext(), command);  
   }  
   public String getOutput() {  
     return (String) getStateHelper().eval("output");  
   }  
   public void setOutput(String output) {  
     getStateHelper().put("output", output);  
   }  
   @Override  
   public String getFamily() {  
     return "qmass.jsf.Console";  
   }  
 }  
Above code does not do much but actually doing it is a bit trickier. We mark this class as a JSF component, and give it an id with @FacesComponent annotation. The means to access your components inputs and outputs are defined with in this class. Get/Set command is for the command that our shell will implement. The output is for printing the result of our shell. And since we will need to perform an action we are extending from UICommand and use it's bindings.
Lastly the renderer :
 @FacesRenderer(rendererType = "UIConsole", componentFamily = "qmass.jsf.Console")  
 @ResourceDependencies({  
     @ResourceDependency(name = "qconsole.css", library = "org.mca.qmass", target = "head"),  
     @ResourceDependency(name = "jsf.js", library = "javax.faces", target = "body")})  
 public class ConsoleRenderer extends Renderer {  
   @Override  
   public void decode(FacesContext context, UIComponent component) {  
     UIConsole console = (UIConsole) component;  
     String val = context.getExternalContext().getRequestParameterMap().get(getCommandId(console));  
     console.setCommand(val);  
     console.queueEvent(new ActionEvent(component));  
   }  
   private String getCommandId(UIConsole console) {  
     return console.getClientId() + "_in";  
   }  
   @Override  
   public void encodeBegin(FacesContext context, UIComponent component) throws IOException {  
     UIConsole comp = (UIConsole) component;  
     String inId = getCommandId(comp);  
     String lines = comp.getOutput();  
     ResponseWriter writer = context.getResponseWriter();  
     writer.startElement("div", null);  
     writer.writeAttribute("id", comp.getClientId(), null);  
     writer.writeAttribute("name", comp.getClientId(), null);  
     writer.writeAttribute("class", "qconsole", null);  
     writer.writeAttribute("onmouseover", "document.getElementById('" + inId + "').focus();", null);  
     String[] lineRay = lines.split("\n");  
     for (int i = 0; i < lineRay.length; i++) {  
       writer.startElement("div", null);  
       writer.writeAttribute("class", "qconsolerow", null);  
       writer.write(lineRay[i].replaceAll(" ", "&nbsp;"));  
       if (i + 1 == lineRay.length) {  
         writer.startElement("input", null);  
         writer.writeAttribute("id", inId, null);  
         writer.writeAttribute("name", inId, null);  
         writer.writeAttribute("onkeypress",  
             "if(event.keyCode == 13){" +  
                 "jsf.ajax.request(this,event,{execute:'" + comp.getClientId() + "'," +  
                 "render:'" + comp.getClientId() + "'," +  
                 "onevent:function(e) {if(e.status=='success')" +  
                 "document.getElementById('" + inId + "').focus();}});" +  
                 "return false;" +  
                 "}",  
             null);  
         writer.writeAttribute("class", "qconsole", null);  
         writer.writeAttribute("autocomplete", "off", null);  
         writer.writeAttribute("type", "text", null);  
         writer.endElement("input");  
       }  
       writer.endElement("div");  
     }  
   }  
   @Override  
   public void encodeEnd(FacesContext context, UIComponent component) throws IOException {  
     ResponseWriter writer = context.getResponseWriter();  
     writer.endElement("div");  
   }  
 }  
We start by defining this class as a Faces Renderer and specify the necessary resources it needs. We need he ajax library and css for custom look. Decode method both sets the new command entered by the user and schedules the action listener attached for execution if necessary. Encode part outputs the HTML we use.
Finally here is how I use the component on a xhtml :
 <?xml version="1.0" encoding="UTF-8"?>  
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"  
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
 <html xmlns="http://www.w3.org/1999/xhtml"  
    xmlns:h="http://java.sun.com/jsf/html"  
    xmlns:q="http://mca.org/qmass">  
 ...  
       <q:console id="q" output="#{consoleBean.output}" command="#{consoleBean.input}"  
             actionListener="#{consoleBean.handleCommand}"/>  
 ...  
 </html>  
You can find out the full source code available here as a part of the QMass project here.
Cheers