Friday, October 7, 2011

Creating a JSF 1.2 Custom Converter with Attributes

Custom converters are a very important part of many JSF applications. Writing and using a basic converter is quite simple if it has no attributes:

<h:outputText value="#{somePhoneNumber}" 
  converter="myPhoneNumberConverter" />

However, things get a little trickier when you need to provide attributes to your converter. For example, Facelets includes a date/time converter:

<h:outputText value="#{someDate}">
  <f:convertDateTime type="both" dateStyle="short"/>
</h:outputText>

While there are many resources out there on creating basic custom converters, I had difficulty finding a good explanation of how to create custom converters with attributes. Here are the steps I followed:

Note: I built this using Seam 2.2 on JBoss EAP 5.1, but this should work for any JSF 1.2 application using Facelets.

The USAPhoneNumber class

We'll be creating a converter for a class called USAPhoneNumber. There's nothing special about this class, just a POJO with an attribute for each "part" of a US phone number.

package org.orr.customconverter;

import java.io.Serializable;

public class USAPhoneNumber implements Serializable {
 private static final long serialVersionUID = 1L;

 private String areaCode;
 private String prefix;
 private String lineNumber;
 private String extension;

 public USAPhoneNumber(String areaCode, String prefix, String lineNumber,
   String extension) {
  super();
  this.areaCode = areaCode;
  this.prefix = prefix;
  this.lineNumber = lineNumber;
  this.extension = extension;
 }

 public USAPhoneNumber(String areaCode, String prefix, String lineNumber) {
  super();
  this.areaCode = areaCode;
  this.prefix = prefix;
  this.lineNumber = lineNumber;
 }

 public String getAreaCode() {
  return areaCode;
 }

 public String getPrefix() {
  return prefix;
 }

 public String getLineNumber() {
  return lineNumber;
 }

 public String getExtension() {
  return extension;
 }

 @Override
 public String toString() {
  String tmp = areaCode + "-" + prefix + "-" + lineNumber;
  if (extension != null && extension.length() > 0)
   tmp += " x" + extension;

  return tmp;
 }
}


Create a Converter class

First we'll create an implementation of javax.faces.convert.Converter. We might want the ability to convert it into a few different styles, such as 212-555-7456, (212) 555-7456, 212 555 7456, etc. To support this, we are creating an attribute called style, which will accept values like parentheses, dashes, and spaces.

package org.orr.customconverter;

import java.io.Serializable;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

public class PhoneNumberConverter implements Converter, Serializable {
 private static final long serialVersionUID = 1L;

 private String style;

 private static final Style DEFAULT_STYLE = Style.DASHES;

 private enum Style {
  DASHES, SPACES, PARENTHESES
 };

 public Object getAsObject(FacesContext context, UIComponent component,
   String stringValue) {

  if (stringValue == null || stringValue.trim().length() == 0)
   return null;

  // We COULD try to read in the value based on the style, but in this
  // case, it's easiest to just strip out all non-numeric characters and
  // require that the number be greater than 10 digits, with any digits
  // past 10 becoming the extension
  String rawNumber = stringValue.replaceAll("[^0-9]", "");

  USAPhoneNumber number = null;

  if (rawNumber.length() < 10)
   throw new ConverterException(new FacesMessage(
     "Phone number must have at least 10 numeric characters"));
  else if (rawNumber.length() == 10)
   number = new USAPhoneNumber(rawNumber.substring(0, 3),
     rawNumber.substring(3, 6), rawNumber.substring(6));
  else
   number = new USAPhoneNumber(rawNumber.substring(0, 3),
     rawNumber.substring(3, 6), rawNumber.substring(6, 10),
     rawNumber.substring(10));

  return number;
 }

 public String getAsString(FacesContext context, UIComponent component,
   Object value) {
  USAPhoneNumber number = (USAPhoneNumber) value;

  if (number == null)
   return "";

  String stringValue = null;

  Style styleEnum = style == null ? DEFAULT_STYLE : Style.valueOf(style
    .toUpperCase());

  switch (styleEnum) {
  case DASHES:
   stringValue = number.getAreaCode() + "-" + number.getPrefix() + "-"
     + number.getLineNumber() + getFormattedExtension(number);
   break;
  case SPACES:
   stringValue = number.getAreaCode() + " " + number.getPrefix() + " "
     + number.getLineNumber() + getFormattedExtension(number);
   break;
  case PARENTHESES:
   stringValue = "(" + number.getAreaCode() + ") "
     + number.getPrefix() + "-" + number.getLineNumber()
     + getFormattedExtension(number);
   break;
  default:
   throw new ConverterException(new FacesMessage("Unsupported style: "
     + style));
  }

  return stringValue;
 }

 private String getFormattedExtension(USAPhoneNumber number) {
  if (number.getExtension() == null)
   return "";
  else
   return " x" + number.getExtension();
 }

 public String getStyle() {
  return style;
 }

 public void setStyle(String style) {
  this.style = style;
 }
}

If you've written a JSF converter before, this will look pretty familiar. However, there are a few things to note:
  1. You must implement java.io.Serializable. After the RENDER RESPONSE phase, JSF serializes the view; in the RENDER RESPONSE phase, it deserializes it. If your converter does not implement Serializable, the attribute(s) (style in this example) will be lost.
  2. You must not make your converter a Seam component. Seam provides some handy annotations to save some of the configuration overhead in creating a converter (see section 33.2 in the Seam reference for details). However, if you are using the same converter with different attribute values on the same page, Seam will reuse the same instance with the same attribute values on the entire page. Note: this might be avoidable by using the STATELESS scope, but I haven't tried it.

Create taglib.xml

Now we need to create a Facelets tag library definition. We'll call it orr-taglib.xml and put it in WebContent/META-INF:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE facelet-taglib PUBLIC
"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
"http://java.sun.com/dtd/facelet-taglib_1_0.dtd">

<facelet-taglib>
 <namespace>http://jerryorr.blogspot.com/customConverterTaglib</namespace>
 <tag>
  <tag-name>convertPhoneNumber</tag-name>
  <converter>
   <converter-id>orr.convertPhoneNumber</converter-id>
  </converter>
 </tag>
</facelet-taglib>

We also need to register the taglib in web.xml:

<context-param>
 <param-name>facelets.LIBRARIES</param-name>
 <param-value>/META-INF/orr-taglib.xml</param-value>
</context-param>

Register converter in faces-config.xml

Next, we need to register our converter in faces-config.xml. Note: this is one of those steps that Seam can save for us, but since we aren't making this a Seam component, we need to register the converter manually.

<converter>
  <converter-id>orr.convertPhoneNumber</converter-id>
  <converter-class>org.orr.customconverter.PhoneNumberConverter</converter-class>
 </converter>

Create a TLD

Finally, we'll create a tag library descriptor. This step is not strictly necessary, but Eclipse will use it for autocomplete.

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.1" 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-jsptaglibrary_2_1.xsd">
 <tlib-version>2.0</tlib-version>
 <short-name>n</short-name>
 <uri>http://jerryorr.blogspot.com/customConverterTaglib</uri>

 <tag>
  <description>Converts a USAPhoneNumber.</description>
  <name>convertPhoneNumber</name>
  <tag-class>org.orr.customconverter.PhoneNumberConverter</tag-class>
  <body-content>JSP</body-content>
  <attribute>
   <description>Style to display the phone number. Valid values include parentheses, dashes, spaces</description>
   <name>style</name>
   <rtexprvalue>true</rtexprvalue>
   <deferred-value>
    <type>java.lang.String</type>
   </deferred-value>
  </attribute>
 </tag>
</taglib>

Using the converter

Now we can use our phone number converter! We'll create a simple Seam component to interact with:

package org.orr.customconverter;

import java.io.Serializable;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.international.StatusMessage.Severity;
import org.jboss.seam.international.StatusMessages;

@Name("updatePhoneNumberAction")
@Scope(ScopeType.SESSION)
public class UpdatePhoneNumberAction implements Serializable {
 private static final long serialVersionUID = 1L;

 private USAPhoneNumber phoneNumber = new USAPhoneNumber("212", "555",
   "3456");

 @In
 StatusMessages statusMessages;

 public USAPhoneNumber getPhoneNumber() {
  return phoneNumber;
 }

 public void setPhoneNumber(USAPhoneNumber phoneNumber) {
  this.phoneNumber = phoneNumber;
 }

 public void update() {
  statusMessages.add(Severity.INFO, "Phone number updated: "
    + phoneNumber);
 }
}

And a simple Facelets view to interact with it:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<f:view xmlns="http://www.w3.org/1999/xhtml"
 xmlns:f="http://java.sun.com/jsf/core"
 xmlns:h="http://java.sun.com/jsf/html"
 xmlns:o="http://jerryorr.blogspot.com/customConverterTaglib"
 contentType="text/html">

 <h:messages />

 <h:form>
  <h:panelGrid columns="2" border="1">
   <h:outputText value="Style" style="font-weight: bold" />
   <h:outputText value="Output" style="font-weight: bold" />

   <h:outputText value="(default)" />
   <h:outputText value="#{updatePhoneNumberAction.phoneNumber}">
    <o:convertPhoneNumber />
   </h:outputText>

   <h:outputText value="parentheses" />
   <h:outputText value="#{updatePhoneNumberAction.phoneNumber}">
    <o:convertPhoneNumber style="parentheses" />
   </h:outputText>

   <h:outputText value="spaces" />
   <h:outputText value="#{updatePhoneNumberAction.phoneNumber}">
    <o:convertPhoneNumber style="spaces" />
   </h:outputText>

   <h:outputText value="dashes" />
   <h:outputText value="#{updatePhoneNumberAction.phoneNumber}">
    <o:convertPhoneNumber style="dashes" />
   </h:outputText>
  </h:panelGrid>

  <p>
   Phone Number:
   <h:inputText value="#{updatePhoneNumberAction.phoneNumber}">
    <o:convertPhoneNumber style="spaces" />
   </h:inputText>
   <h:commandButton action="#{updatePhoneNumberAction.update()}"
    value="Update" />
  </p>
 </h:form>
</f:view>

When we first load the page, we can see our converter in action:


And since we have our converter on the inputText component, we can see it converter back to a USAPhoneNumber when we add an extension:


Hopefully, all of this will be a lot easier in future versions of JSF. For those of us stuck on JSF 1.2, though, creating our own converters with attributes can come in handy!

1 comment:

  1. Thank Jelly. I am looking for a searchable autoComplete selectonemenu but am un-able to write a custom converter and iterator for such a functionality. Any help? Thanks in advance.

    ReplyDelete