Thursday, October 13, 2011

Replacement for s:decorate in Seam 3

I've been doing a gap analysis for our migration from Seam 2 to Seam 3, and I was dismayed to find that the <s:decorate> tag is gone! In Seam 2, you create a template like this:

<ui:composition  xmlns="http://www.w3.org/1999/xhtml"
                 xmlns:ui="http://java.sun.com/jsf/facelets"
                 xmlns:h="http://java.sun.com/jsf/html"
                 xmlns:f="http://java.sun.com/jsf/core"
                 xmlns:s="http://jboss.com/products/seam/taglib">

    <div class="prop">
        <s:label styleClass="nameEdit #{invalid?'errors':''}">
            <s:span styleClass="required"
               rendered="#{required}">*</s:span>
            <ui:insert name="label"/>
        </s:label>

        <span class="value #{invalid?'errors':''}">
            <s:validateAll>
                <ui:insert/>
            </s:validateAll>
        </span>

        <span class="error">
            <h:graphicImage value="/img/error.gif" 
                rendered="#{invalid}" styleClass="errors"/>
        </span>
    </div>
</ui:composition>

And then reference it using <s:decorate>

<s:decorate template="edit.xhtml">
  <ui:define name="label">Country:</ui:define>
  <h:inputText value="#{location.country}" required="true"/>
</s:decorate>

And now required fields are noted with an asterisk (*), all fields are automatically validated, and when they have errors, a special style is applied and the error message appears to the right of the field:


Note: this is all documented in Section 33.1.13 of the Seam 2 reference.

Pretty slick! I definitely need this functionality when I migrate to Seam 3, but the <s:decorate> tag is gone, and I had a hard time finding a replacement.

Replacing with UIInputContainer

Fortunately, a close replacement actually exists in Seam 3's Faces module... it just isn't described as such! Seam Faces provides a component called UIInputContainer. When this is combined with a JSF 2 composite component, you can get the same functionality.

First, create your composite component. I created mine at WebContent/resources/orr/decorate.xhtml:

<?xml version="1.0"?>
<!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:composite="http://java.sun.com/jsf/composite">

<composite:interface componentType="org.jboss.seam.faces.InputContainer" />

<composite:implementation>
 <div>
  <h:outputLabel id="label" value="#{cc.attrs.label}:"
   styleClass="#{cc.attrs.invalid ? 'invalid' : ''}">

   <h:outputText styleClass="required" rendered="#{cc.attrs.required}"
    value="*" />

  </h:outputLabel>

  <!-- h:panelGroup is a workaround for a JSF bug, see http://java.net/jira/browse/JAVASERVERFACES-1991  -->
  <h:panelGroup styleClass="value #{invalid?'errors':''}" >
   <composite:insertChildren />
  </h:panelGroup>

  <h:message id="message" errorClass="invalid message"
   rendered="#{cc.attrs.invalid}" />
 </div>
</composite:implementation>
</html>

Since I put the composite component in WebContent/resources/orr/decorate.xhtml, the namespace is http://java.sun.com/jsf/composite/orr and the tag name is decorate.

Now use this new tag in your Facelets page:
<!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:f="http://java.sun.com/jsf/core"
 xmlns:o="http://java.sun.com/jsf/composite/orr">



<o:decorate label="Country:">
    <h:inputText value="#{location.country}" required="true"/>
</o:decorate>

And that's it! This will generate roughly the same output and behavior as Seam 2's <s:decorate> tag. So all along, there was a pretty good replacement, but either this wasn't made clear anywhere, or my Google skills aren't quite as good as I think. I imagine this would be documented in JBoss's Seam 2 to Seam 3 migration guide, if such a thing existed...

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!