Chapter 7. Event Handling

Programming with Custom Converters and Validators

JSF standard converters and validators cover a lot of bases, but many web applications must go further. For example, you may need to convert to types other than numbers and dates or perform application-specific validation, such as checking a credit card.

In the following sections, we show you how to implement application-specific converters and validators. These implementations require a moderate amount of programming.

Implementing Custom Converter Classes

A converter is a class that converts between strings and objects. A converter must implement the Converter interface, which has the following two methods:

  Object getAsObject(FacesContext context, UIComponent component, String newValue)   String getAsString(FacesContext context, UIComponent component, Object value)     

The first method converts a string into an object of the desired type, throwing a ConverterException if the conversion cannot be carried out. This method is called when a string is submitted from the client, typically in a text field. The second method converts an object into a string representation to be displayed in the client interface.

To illustrate these methods, we develop a custom converter for credit card numbers. Our converter allows users to enter a credit card number with or without spaces. That is, we accept inputs of the following forms:

  1234567890123456   1234 5678 9012 3456

Listing 6-6 shows the code for the custom converter. The getAsObject method of the converter strips out all characters that are not digits. It then creates an object of type CreditCard. If an error is found, then we generate a FacesMessage object and throw a ConverterException. We will discuss these steps in the next section, "Reporting Conversion Errors," on page 245.

The getAsString method of our converter makes an effort to format the credit card number in a way that is pleasing to the eye of the user. The digits are separated into the familiar patterns, depending on the credit card type. Table 6-6 shows the most common credit card formats.

Table 6-6. Credit Card Formats
Card Type Digits Format
MasterCard 16 5xxx xxxx xxxx xxxx
Visa 16 4xxx xxxx xxxx xxxx
Visa 13 4xxx xxx xxx xxx
Discover 16 6xxx xxxx xxxx xxxx
American Express 15 37xx xxxxxx xxxxx
American Express 22 3xxxxx xxxxxxxx xxxxxxxx
Diners Club, Carte Blanche 14 3xxxx xxxx xxxxx


In this example, the CreditCard class is minor; it contains just the credit card number (see Listing 6-7). We could have left the credit card number as a String object, reducing the converter to a formatter. However, most converters have a target type other than String. To make it easier for you to reuse this example, we use a distinct target type.

Listing 6-6. converter2/src/java/com/coresjf/CreditCardConverter.java

  1. package com.corejsf;   2.   3. import javax.faces.application.FacesMessage;   4. import javax.faces.component.UIComponent;   5. import javax.faces.context.FacesContext;   6. import javax.faces.convert.Converter;   7. import javax.faces.convert.ConverterException;   8.   9. public class CreditCardConverter implements Converter {  10.    public Object getAsObject(FacesContext context, UIComponent component,  11.          String newValue) throws ConverterException {  12.       StringBuilder builder = new StringBuilder(newValue);  13.       boolean foundInvalidCharacter = false;  14.       char invalidCharacter = '\0';   15.       int i = 0;  16.       while (i < builder.length() && !foundInvalidCharacter) {  17.          char ch = builder.charAt(i);  18.          if (Character.isDigit(ch))  19.             i++;  20.          else if (Character.isWhitespace(ch))  21.             builder.deleteCharAt(i);  22.          else {  23.             foundInvalidCharacter = true;  24.             invalidCharacter = ch;  25.          }  26.     }  27.  28.     if (foundInvalidCharacter) {  29.        FacesMessage message = com.corejsf.util.Messages.getMessage(  30.              "com.corejsf.messages", "badCreditCardCharacter",  31.              new Object[]{ new Character(invalidCharacter) });  32.        message.setSeverity(FacesMessage.SEVERITY_ERROR);  33.        throw new ConverterException(message);  34.     }  35.  36.     return new CreditCard(builder.toString());  37.   }  38.  39.   public String getAsString(FacesContext context, UIComponent component,  40.         Object value) throws ConverterException {  41.      // length 13: xxxx xxx xxx xxx  42.      // length 14: xxxxx xxxx xxxxx  43.      // length 15: xxxx xxxxxx xxxxx  44.      // length 16: xxxx xxxx xxxx xxxx  45.      // length 22: xxxxxx xxxxxxxx xxxxxxxx  46.     String v = value.toString();  47.     int[] boundaries = null;  48.     int length = v.length();  49.     if (length == 13)  50.        boundaries = new int[]{ 4, 7, 10 };  51.     else if (length == 14)  52.        boundaries = new int[]{ 5, 9 };  53.     else if (length == 15)  54.        boundaries = new int[]{ 4, 10 };  55.     else if (length == 16)  56.        boundaries = new int[]{ 4, 8, 12 };  57.     else if (length == 22)  58.        boundaries = new int[]{ 6, 14 };  59.     else  60.        return v;  61.     StringBuilder result = new StringBuilder();  62.     int start = 0;  63.     for (int i = 0; i < boundaries.length; i++) {  64.        int end = boundaries[i];  65.        result.append(v.substring(start, end));  66.        result.append(" ");  67.        start = end;  68.     }  69.     result.append(v.substring(start));  70.     return result.toString();  71.   }  72. }     

Listing 6-7. converter2/src/java/com/corejsf/CreditCard.java

  1. package com.corejsf;   2.   3. public class CreditCard {   4.    private String number;   5.   6.    public CreditCard(String number) { this.number = number; }   7.    public String toString() { return number; }   8. }

Specifying Converters

One mechanism for specifying converters involves a symbolic ID that you register with the JSF application. We will use the ID com.corejsf.CreditCard for our credit card converter. The following entry to faces-config.xml associates the converter ID with the class that implements the converter:

  <converter>      <converter-id>com.corejsf.CreditCard</converter-id>      <converter-class>com.corejsf.CreditCardConverter</converter-class>   </converter>

In the following examples, we will assume that the card property of the PaymentBean has type CreditCard, as shown in Listing 6-13 on page 254. Now we can use the f:converter tag and specify the converter ID:

  <h:inputText value="#{payment.card}">      <f:converter converter/>   </h:inputText>

Or, more succinctly, we can use the converter attribute:

  <h:inputText value="#{payment.card}" converter="com.corejsf.CreditCard"/>

You can also access a converter without defining it in a configuration file. Use the converter attribute with a value expression that yields the converter object:

  <h:outputText value="#{payment.card}" converter="#{bb.convert}"/>

Here, the bb bean must have a convert property of type Converter.

If you like, you can implement the property getter so that it returns an inner class object:

  public class BackingBean {      ...      public Converter getConvert() {         return new Converter() {            public Object getAsObject(FacesContext context, UIComponent component,               String newValue) throws ConverterException { ... }            public String getAsString(FacesContext context, UIComponent component,               Object value) throws ConverterException { ... }         };      }   }     

This approach is convenient because the conversion methods can access the bean's private data.

Alternatively, if you are confident that your converter is appropriate for all conversions between String and CreditCard objects, then you can register it as the default converter for the CreditCard class:

  <converter>       <converter-for-class>com.corejsf.CreditCard</converter-for-class>      <converter-class>com.corejsf.CreditCardConverter</converter-class>   </converter>

Now you do not have to mention the converter any longer. It is automatically used whenever a value reference has the type CreditCard. For example, consider the tag

  <h:inputText value="#{payment.card}"/>

When the JSF implementation converts the request value, it notices that the target type is CreditCard, and it locates the converter for that class. This is the ultimate in converter convenience for the page author!

javax.faces.convert.Converter

  • Object getAsObject(FacesContext context, UIComponent component, String value)

    Converts the given string value into an object that is appropriate for storage in the given component.

  • String getAsString(FacesContext context, UIComponent component, Object value)

    Converts the given object, which is stored in the given component, into a string representation.


Reporting Conversion Errors

When a converter detects an error, it should throw a ConverterException. For example, the getAsObject method of our credit card converter checks whether the credit card contains characters other than digits or separators. If it finds an invalid character, it signals an error:

  if (foundInvalidCharacter) {      FacesMessage message = new FacesMessage(         "Conversion error occurred. ", "Invalid card number. ");      message.setSeverity(FacesMessage.SEVERITY_ERROR);      throw new ConverterException(message);    }

The FacesMessage object contains the summary and detail messages that can be displayed with message tags.

javax.faces.application.FacesMessage

  • FacesMessage(FacesMessage.Severity severity, String summary, String detail)

    Constructs a message with the given severity, summary, and detail. The severity is one of the constants SEVERITY_ERROR, SEVERITY_FATAL, SEVERITY_INFO, or SEVERITY_WARN in the FacesMessage class.

  • FacesMessage(String summary, String detail)

    Constructs a message with severity SEVERITY_INFO and the given summary and detail.

  • void setSeverity(FacesMessage.Severity severity)

    Sets the severity to the given level. The severity is one of the constants SEVERITY_ERROR, SEVERITY_FATAL, SEVERITY_INFO, or SEVERITY_WARN in the FacesMessage class.


javax.faces.convert.ConverterException

  • ConverterException(FacesMessage message)

  • ConverterException(FacesMessage message, Throwable cause)

    These constructors create exceptions whose getMessage method returns the summary of the given message and whose getFacesMessage method returns the given message.

  • ConverterException()

  • ConverterException(String detailMessage)

  • ConverterException(Throwable cause)

  • ConverterException(String detailMessage, Throwable cause)

    These constructors create exceptions whose getMessage method returns the given detail message and whose getFacesMessage method returns null.

  • FacesMessage getFacesMessage()

    Returns the FacesMessage with which this exception object was constructed or returns null if none was supplied.


Getting Error Messages from Resource Bundles

Of course, for proper localization, you will want to retrieve the error messages from a message bundle.

Doing that involves some busywork with locales and class loaders:

  1. Get the current locale.

    FacesContext context = FacesContext.getCurrentInstance(); UIViewRoot viewRoot = context.getViewRoot(); Locale locale = viewRoot.getLocale();
  2. Get the current class loader. You need it to locate the resource bundle.

    ClassLoader loader = Thread.currentThread().getContextClassLoader();
  3. Get the resource bundle with the given name, locale, and class loader.

    ResourceBundle bundle = ResourceBundle.getBundle(bundleName, locale, loader);     
  4. Get the resource string with the given ID from the bundle.

    String resource = bundle.getString(resourceId);

However, there are several wrinkles in the process. We actually need two message strings: one for the summary and one for the detail messages. By convention, the resource ID of a detail message is obtained by addition of the string _detail to the summary key. For example,

  badCreditCardCharacter=Invalid card number.    badCreditCardCharacter_detail=The card number contains invalid characters.     

Moreover, converters are usually part of a reusable library. It is a good idea to allow a specific application to override messages. (You saw in "Changing the Text of Standard Error Messages" on page 228 how to override the standard converter messages.) Therefore, you should first attempt to locate the messages in the application-specific message bundle before retrieving the default messages.

Recall that an application can supply a bundle name in a configuration file, such as

  <faces-config>      <application>         <message-bundle>com.mycompany.myapp.messages</message-bundle>      </application>      ...    </faces-config>

The following code snippet retrieves that bundle name:

  Application app = context.getApplication();   String appBundleName = app.getResourceBundle();

Look up your resources in this bundle before going to the library default.

Finally, you may want some messages to provide detailed information about the nature of the error. For example, you want to tell the user which character in the credit card number was objectionable. Message strings can contain placeholders {0}, {1}, and so on for example:

  The card number contains the invalid character {0}.

The java.text.MessageFormat class can substitute values for the placeholders:

  Object[] params = ...;   MessageFormat formatter = new MessageFormat(resource, locale);   String message = formatter.format(params);

Here, the params array contains the values that should be substituted. (For more information about the MessageFormat class, see Horstmann and Cornell, 2004, 2005. Core Java 2, vol. 2, chap. 10.)

Ideally, much of this busywork should have been handled by the JSF framework. Of course, you can find the relevant code in the innards of the reference implementation, but the framework designers chose not to make it available to JSF programmers.

We provide the package com.corejsf.util with convenience classes that implement these missing pieces. Feel free to use these classes in your own code.

The com.corejsf.util.Messages class has a static method, getMessage, that returns a FacesMessage with a given bundle name, resource ID, and parameters:

  FacesMessage message       = com.corejsf.util.Messages.getMessage(         "com.corejsf.messages", "badCreditCardCharacter",         new Object[] { new Character(invalidCharacter) });

You can pass null for the parameter array if the message does not contain placeholders.

Our implementation follows the JSF convention of displaying missing resources as ???resourceId???. See Listing 6-8 for the source code.

Note

If you prefer to reuse the standard JSF message for conversion errors, call

   FacesMessage message = com.corejsf.util.Messages.getMessage(       "javax.faces.Messages",        "javax.faces.component.UIInput.CONVERSION",        null);


javax.faces.context.FacesContext

  • static FacesContext getCurrentInstance()

    Gets the context for the request that is being handled by the current thread, or null if the current thread does not handle a request.

  • UIViewRoot getViewRoot()

    Gets the root component for the request described by this context.


javax.faces.component.UIViewRoot

  • Locale getLocale()

    Gets the locale for rendering this view.


Listing 6-8. converter2/src/java/com/corejsf/util/Messages.java

  1. package com.corejsf.util;   2.   3. import java.text.MessageFormat;   4. import java.util.Locale;   5. import java.util.MissingResourceException;   6. import java.util.ResourceBundle;   7. import javax.faces.application.Application;   8. import javax.faces.application.FacesMessage;   9. import javax.faces.component.UIViewRoot;  10. import javax.faces.context.FacesContext;  11.  12. public class Messages {  13     public static FacesMessage getMessage(String bundleName, String resourceId,  14        Object[] params) {  15        FacesContext context = FacesContext.getCurrentInstance();  16        Application app = context.getApplication();  17        String appBundle = app.getMessageBundle();  18        Locale locale = getLocale(context);  19        ClassLoader loader = getClassLoader();  20        String summary = getString(appBundle, bundleName, resourceId,   21           locale, loader, params);  22.       if (summary == null) summary = "???" + resourceId + "???";  23.       String detail = getString(appBundle, bundleName, resourceId + "_detail",   24.          locale, loader, params);  25.       return new FacesMessage(summary, detail);  26.     }  27.  28.     public static String getString(String bundle, String resourceId,   29.           Object[] params) {  30.        FacesContext context = FacesContext.getCurrentInstance();  31.        Application app = context.getApplication();  32.        String appBundle = app.getMessageBundle();  33.        Locale locale = getLocale(context);  34.        ClassLoader loader = getClassLoader();  35.        return getString(appBundle, bundle, resourceId, locale, loader, params);  36.     }  37.  38.     public static String getString(String bundle1, String bundle2,   39.           String resourceId, Locale locale, ClassLoader loader,   40.           Object[] params) {  41.        String resource = null;  42.        ResourceBundle bundle;  43.  44.        if (bundle1 != null) {  45.           bundle = ResourceBundle.getBundle(bundle1, locale, loader);  46.           if (bundle != null)  47.              try {  48.                 resource = bundle.getString(resourceId);  49.              } catch (MissingResourceException ex) {  50.              }  51.        }  52.  53.        if (resource == null) {  54.           bundle = ResourceBundle.getBundle(bundle2, locale, loader);  55.           if (bundle != null)  56.              try {  57.                  resource = bundle.getString(resourceId);  58.              } catch (MissingResourceException ex) {  59.              }  60.         }  61.  62.         if (resource == null) return null; // no match  63.         if (params == null) return resource;  64.  65.         MessageFormat formatter = new MessageFormat(resource, locale);  66.         return formatter.format(params);  67.       }  68.  69.       public static Locale getLocale(FacesContext context) {  70.          Locale locale = null;  71.          UIViewRoot viewRoot = context.getViewRoot();  72.          if (viewRoot != null) locale = viewRoot.getLocale();  73.          if (locale == null) locale = Locale.getDefault();  74.          return locale;  75.       }  76.  77.       public static ClassLoader getClassLoader() {  78.          ClassLoader loader = Thread.currentThread().getContextClassLoader();  79.          if (loader == null) loader = ClassLoader.getSystemClassLoader();  80.          return loader;  81.      }  82. }     

The Custom Converter Sample Application

Here are the remaining pieces of our next sample application. Figure 6-8 shows the directory structure. Listings 6-9 and 6-10 show the input and result pages. Look at the inputText and outputText tags for the credit card numbers to see the two styles of specifying a custom converter. (Both converter specifications could have been omitted if the converter had been registered to be the default for the CreditCard type.)

Figure 6-8. Directory structure of the custom converter example


The custom converter is defined in the faces-config.xml file (Listing 6-11). The messages.properties file (shown in Listing 6-12) contains the error message for the credit card converter. Finally, Listing 6-13 shows the payment bean with three properties of type double, Date, and CreditCard.

Listing 6-9. converter2/web/index.jsp

  1. <html>   2.    <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>   3.    <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>   4.    <f:view>   5.       <head>   6.          <link href="styles.css" rel="stylesheet" type="text/css"/>   7.          <title><h:outputText value="#{msgs.title}"/></title>   8.       </head>   9.       <body>  10.          <h:form>  11.             <h1><h:outputText value="#{msgs.enterPayment}"/></h1>  12.             <h:panelGrid columns="3">  13.                <h:outputText value="#{msgs.amount}"/>  14.                <h:inputText  label="#{msgs.amount}"  15.                      value="#{payment.amount}">  16.                   <f:convertNumber minFractionDigits="2"/>  17.                </h:inputText>  18.                <h:message for="amount" style/>  19.  20.                <h:outputText value="#{msgs.creditCard}"/>  21.                <h:inputText  label="#{msgs.creditCard}"  22.                      value="#{payment.card}">  23.                   <f:converter converter/>  24.                </h:inputText>  25.                <h:message for="card" style/>  26.  27.                <h:outputText value="#{msgs.expirationDate}"/>  28.                <h:inputText  label="#{msgs.expirationDate}"  29.                      value="#{payment.date}">  30.                   <f:convertDateTime pattern="MM/yyyy"/>  31.                </h:inputText>  32.                <h:message for="date" style/>  33.             </h:panelGrid>  34.             <h:commandButton value="Process" action="process"/>  35.          </h:form>  36.       </body>  37.    </f:view>  38. </html>     

Listing 6-10. converter2/web/result.jsp

  1. <html>   2.    <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>   3.    <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>   4.    <f:view>   5.       <head>   6.          <link href="styles.css" rel="stylesheet" type="text/css"/>   7.          <title><h:outputText value="#{msgs.title}"/></title>   8.       </head>   9.       <body>  10.          <h:form>  11.             <h1><h:outputText value="#{msgs.paymentInformation}"/></h1>  12.             <h:panelGrid columns="2">  13.                <h:outputText value="#{msgs.amount}"/>  14.                <h:outputText value="#{payment.amount}">   15.                   <f:convertNumber type="currency"/>  16.                </h:outputText>  17.  18.                <h:outputText value="#{msgs.creditCard}"/>  19.                <h:outputText value="#{payment.card}"   20.                   converter="com.corejsf.CreditCard"/>  21.  22.                <h:outputText value="#{msgs.expirationDate}"/>  23.                <h:outputText value="#{payment.date}">   24.                   <f:convertDateTime pattern="MM/yyyy"/>  25.                </h:outputText>  26.             </h:panelGrid>  27.             <h:commandButton value="Back" action="back"/>  28.          </h:form>  29.       </body>  30.    </f:view>  31. </html>     

Listing 6-11. converter2/web/WEB-INF/faces-config.xml

  1. <?xml version="1.0"?>   2. <faces-config xmlns="http://java.sun.com/xml/ns/javaee"   3.    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   4.    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee    5.         http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"   6.    version="1.2">   7.    <application>   8.       <message-bundle>com.corejsf.messages</message-bundle>   9.    </application>  10.  11.    <navigation-rule>  12.       <from-view-id>/index.jsp</from-view-id>  13.       <navigation-case>  14.          <from-outcome>process</from-outcome>  15.          <to-view-id>/result.jsp</to-view-id>  16.       </navigation-case>  17.    </navigation-rule>  18.  19.    <navigation-rule>  20.       <from-view-id>/result.jsp</from-view-id>  21.       <navigation-case>  22.          <from-outcome>back</from-outcome>  23.          <to-view-id>/index.jsp</to-view-id>  24.       </navigation-case>  25.    </navigation-rule>  26.  27.    <converter>   28.       <converter-id>com.corejsf.CreditCard</converter-id>  29.       <converter-class>com.corejsf.CreditCardConverter</converter-class>   30.    </converter>  31.  32.    <managed-bean>   33.       <managed-bean-name>payment</managed-bean-name>  34.       <managed-bean-class>com.corejsf.PaymentBean</managed-bean-class>   35.       <managed-bean-scope>session</managed-bean-scope>   36.    </managed-bean>  37.  38.   <application>  39.      <resource-bundle>  40.         <base-name>com.corejsf.messages</base-name>  41.         <var>msgs</var>  42.      </resource-bundle>  43.   </application>  44. </faces-config>     

Listing 6-12. converter2/src/java/com/corejsf/messages.properties

  1. badCreditCardCharacter=Invalid card number.    2. badCreditCardCharacter_detail=The card number contains the invalid character {0}.    3. title=An Application to Test Data Conversion   4. enterPayment=Please enter the payment information   5. amount=Amount   6. creditCard=Credit Card   7. expirationDate=Expiration date (Month/Year)   8. process=Process   9. paymentInformation=Payment information     

Listing 6-13. converter2/src/java/com/corejsf/PaymentBean.java

  1. package com.corejsf;   2. import java.util.Date;   3.   4. public class PaymentBean {   5.    private double amount;   6.    private CreditCard card = new CreditCard("");   7.    private Date date = new Date();   8.   9.    // PROPERTY: amount  10.    public void setAmount(double newValue) { amount = newValue; }  11.    public double getAmount() { return amount; }  12.  13.    // PROPERTY: card  14.    public void setCard(CreditCard newValue) { card = newValue; }  15.    public CreditCard getCard() { return card; }  16.  17.    // PROPERTY: date  18.    public void setDate(Date newValue) { date = newValue; }  19.    public Date getDate() { return date; }  20. }

Implementing Custom Validator Classes

Implementing custom validator classes is a two-step process, similar to the process you saw in the preceding section:

  1. Implement a validator by implementing the javax.faces.validator.Validator interface.

  2. Register your validator in a configuration file (such as faces-config.xml).

The Validator interface defines only one method:

  void validate(FacesContext context, UIComponent component, Object value)

If validation fails, generate a FacesMessage that describes the error, construct a ValidatorException from the message, and throw it:

   if (validation fails) {       FacesMessage message = ...;       message.setSeverity(FacesMessage.SEVERITY_ERROR);       throw new ValidatorException(message);   }

The process is analogous to the reporting of conversion errors, except that you throw a ValidatorException instead of a ConverterException.

For example, Listing 6-14 shows a validator that checks the digits of a credit card, using the Luhn formula. Figure 6-9 shows the application at work. As described under "Getting Error Messages from Resource Bundles" on page 246, we use the convenience class com.corejsf.util.Messages to locate the message strings in a resource bundle.

Figure 6-9. Luhn check failed


Note

The Luhn formula developed by a group of mathematicians in the late 1960s verifies and generates credit card numbers, as well as Social Insurance numbers for the Canadian government. The formula can detect whether a digit is entered wrongly or whether two digits were transposed. See the web site http://www.merriampark.com/anatomycc.htm for more information about the Luhn formula. For debugging, it is handy to know that the number 4111 1111 1111 1111 passes the Luhn check.


javax.faces.validator.Validator

  • void validate(FacesContext context, UIComponent component, Object value)

    Validates the component to which this validator is attached. If there is a validation error, throw a ValidatorException.


Listing 6-14. validator2/src/java/com/corejsf/CreditCardValidator.java

  1. package com.corejsf;   2.   3. import javax.faces.application.FacesMessage;   4. import javax.faces.component.UIComponent;   5. import javax.faces.context.FacesContext;   6. import javax.faces.validator.Validator;   7. import javax.faces.validator.ValidatorException;   8.   9. public class CreditCardValidator implements Validator {  10.    public void validate(FacesContext context, UIComponent component,  11.          Object value) {  12.       if(value == null) return;  13.       String cardNumber;  14.       if (value instanceof CreditCard)  15.          cardNumber = value.toString();  16.       else  17.          cardNumber = getDigitsOnly(value.toString());  18.       if(!luhnCheck(cardNumber)) {  19.          FacesMessage message  20.             = com.corejsf.util.Messages.getMessage(  21.                "com.corejsf.messages", "badLuhnCheck", null);  22.          message.setSeverity(FacesMessage.SEVERITY_ERROR);  23.          throw new ValidatorException(message);  24.        }  25.     }  26.  27.    private static boolean luhnCheck(String cardNumber) {  28.       int sum = 0;  29.  30.       for(int i = cardNumber.length() - 1; i >= 0; i -= 2) {  31.          sum += Integer.parseInt(cardNumber.substring(i, i + 1));  32.          if(i > 0) {  33.             int d = 2 * Integer.parseInt(cardNumber.substring(i - 1, i));  34.             if(d > 9) d -= 9;  35.             sum += d;  36.          }  37.       }  38.  39.       return sum % 10 == 0;  40.    }  41.  42.    private static String getDigitsOnly(String s) {  43.       StringBuilder digitsOnly = new StringBuilder ();  44.       char c;  45.       for(int i = 0; i < s.length (); i++) {  46.          c = s.charAt (i);  47.          if (Character.isDigit(c)) {  48.             digitsOnly.append(c);  49.          }  50.       }  51.       return digitsOnly.toString ();  52.    }  53. }     

Registering Custom Validators

Now that we have created a validator, we need to register it in a configuration file (such as faces-config.xml), like this:

  <validator>      <validator-id>com.corejsf.CreditCard</validator-id>      <validator-class>com.corejsf.CreditCardValidator</validator-class>   </validator>

You can use custom validators with the f:validator tag for example, the following code fragment uses the credit card validator discussed above:

  <h:inputText  value="#{payment.card}" required="true">      <f:converter converter/>      <f:validator validator/>   </h:inputText>

The validatorId specified for f:validator must correspond to a validator ID specified in the configuration file. The f:validator tag uses the validator ID to look up the corresponding class, creates an instance of that class if necessary, and invokes its validate method.

Note

JSF uses separate name spaces for converter and validator IDs. Thus, it is okay to have both a converter and a validator with the ID com.corejsf.CreditCard.


Note

JSF registers its three standard validators with IDs javax.faces.LongRange, javax.faces.DoubleRange, and javax.faces.Length.


The remainder of the sample application is straightforward. Figure 6-10 shows the directory structure, and Listing 6-15 contains the JSF page.

Figure 6-10. The directory structure of the Luhn check example


Listing 6-15. validator2/web/index.jsp

  1. <html>   2.    <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>   3.    <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>   4.    <f:view>   5.       <head>   6.          <link href="styles.css" rel="stylesheet" type="text/css"/>   7.          <title><h:outputText value="#{msgs.title}"/></title>   8.       </head>   9.       <body>  10.          <h:form>  11.             <h1><h:outputText value="#{msgs.enterPayment}"/></h1>  12.             <h:panelGrid columns="3">  13.                <h:outputText value="#{msgs.amount}"/>  14.                <h:inputText  label="#{msgs.amount}"  15.                      value="#{payment.amount}">  16.                   <f:convertNumber minFractionDigits="2"/>  17.                </h:inputText>  18.                <h:message for="amount" style/>  19.  20.                <h:outputText value="#{msgs.creditCard}"/>  21.                <h:inputText  label="#{msgs.creditCard}"  22.                      value="#{payment.card}" required="true">  23.                   <f:converter converter/>  24.                   <f:validator validator/>  25.                </h:inputText>  26.                <h:message for="card" style/>  27.  28.                <h:outputText value="#{msgs.expirationDate}"/>  29.                <h:inputText  label="#{msgs.expirationDate}"  30.                      value="#{payment.date}">  31.                  <f:convertDateTime pattern="MM/yyyy"/>  32.               </h:inputText>  33.               <h:message for="date" style/>  34.            </h:panelGrid>  35.            <h:commandButton value="Process" action="process"/>  36.         </h:form>  37.      </body>  38.   </f:view>  39. </html>     

The f:validator tag is useful for simple validators that do not have parameters, such as the credit validator discussed above. If you need a validator with properties that can be specified in a JSF page, you should implement a custom tag for your validator. You will see how to do that in "Implementing Custom Converters and Validators" on page 432 of Chapter 9.

Validating with Bean Methods

In the preceding section, you saw how to implement a validation class. However, you can also add the validation method to an existing class and invoke it through a method expression, like this:

  <h:inputText  value="#{payment.card}"      required="true" validator="#{payment.luhnCheck}"/>

The payment bean must then have a method with the exact same signature as the validate method of the Validator interface:

  public class PaymentBean {      ...      public void luhnCheck(FacesContext context, UIComponent component, Object value) {         ... // same code as in the preceding example      }   }     

Why would you want to do this? There is one major advantage. The validation method can access other instance fields of the class. You will see an example in the next section, "Supplying Attributes to Converters."

On the downside, this approach makes it more difficult to move a validator to a new web application, so you would probably only use it for application-specific scenarios.

Caution

The value of the validator attribute is a method expression, whereas the seemingly similar converter attribute specifies a converter ID (if it is a string) or a converter object (if it is a value expression). As Emerson said, "A foolish consistency is the hobgoblin of little minds."


Supplying Attributes to Converters

Every JSF component can store arbitrary attributes. You can set an attribute of the component to which you attach a converter; use the f:attribute tag. Your converter can then retrieve the attribute from its component. Here is how that technique would work to set the separator string for the credit card converter.

When attaching the converter, also nest an f:attribute tag inside the component:

  <h:outputText value="#{payment.card}">      <f:converter converter/>      <f:attribute name="separator" value="-"/>   </h:outputText>

In the converter, retrieve the attribute as follows:

  separator = (String) component.getAttributes().get("separator");

In Chapter 9, you will see a more elegant mechanism for passing attributes to a converter writing your own converter tag.

javax.faces.component.UIComponent

  • Map getAttributes()

    Returns a mutable map of all attributes and properties of this component.


Validating Relationships Between Multiple Components

The validation mechanism in JSF was designed to validate a single component. However, in practice, you often need to ensure that related components have reasonable values before letting the values propagate into the model. For example, as we noted earlier, it is not a good idea to ask users to enter a date into a single text field. Instead, you would use three different text fields, for the day, month, and year, as in Figure 6-11.

Figure 6-11. Validating a relationship involving three components


If the user enters an illegal date, such as February 30, you would want to show a validation error and prevent the illegal data from entering the model.

The trick is to attach the validator to the last of the components. By the time its validator is called, the preceding components have passed validation and had their local values set. The last component has passed conversion, and the converted value is passed as the Object parameter of the validation method.

Of course, you need to have access to the other components. You can easily achieve that access by using a backing bean that contains all components of the current form (see Listing 6-16). Attach the validation method to the backing bean:

  public class BackingBean {      private UIInput dayInput;      private UIInput monthInput;      ...      public void validateDate(FacesContext context, UIComponent component,          Object value) {         int d = ((Integer) dayInput.getLocalValue()).intValue();         int m = ((Integer) monthInput.getLocalValue()).intValue();         int y = ((Integer) value).intValue();         if (!isValidDate(d, m, y)) {            FacesMessage message = ...;            throw new ValidatorException(message);         }      }      ...   }

Note that the value lookup is a bit asymmetric. The last component does not yet have the local value set because it has not passed validation.

Figure 6-12 shows the application's directory structure. Listing 6-17 shows the JSF page. Note the converter property of the last input field. Also note the use of the binding attributes that bind the input components to the backing bean.

Figure 6-12. Directory structure of the date validation example


Listing 6-16. validator3/src/java/com/corejsf/BackingBean.java

  1. package com.corejsf;   2.   3.  import javax.faces.application.FacesMessage;   4.  import javax.faces.component.UIComponent;   5.  import javax.faces.component.UIInput;   6.  import javax.faces.context.FacesContext;   7.  import javax.faces.validator.ValidatorException;   8.   9.  public class BackingBean {  10.     private int day;  11.     private int month;  12.     private int year;  13.     private UIInput dayInput;  14.     private UIInput monthInput;  15.     private UIInput yearInput;  16.  17.     // PROPERTY: day  18.     public int getDay() { return day; }  19.     public void setDay(int newValue) { day = newValue; }  20.  21.     // PROPERTY: month  22.     public int getMonth() { return month; }  23.     public void setMonth(int newValue) { month = newValue; }  24.  25.     // PROPERTY: year  26.     public int getYear() { return year; }  27.     public void setYear(int newValue) { year = newValue; }  28.  29.     // PROPERTY: dayInput  30.     public UIInput getDayInput() { return dayInput; }  31.     public void setDayInput(UIInput newValue) { dayInput = newValue; }  32.  33.     // PROPERTY: monthInput  34.     public UIInput getMonthInput() { return monthInput; }  35.     public void setMonthInput(UIInput newValue) { monthInput = newValue; }  36.  37.     // PROPERTY: yearInput  38.     public UIInput getYearInput() { return yearInput; }  39.     public void setYearInput(UIInput newValue) { yearInput = newValue; }  40.  41.     public void validateDate(FacesContext context, UIComponent component,   42.        Object value) {  43.        int d = ((Integer) dayInput.getLocalValue()).intValue();  44.        int m = ((Integer) monthInput.getLocalValue()).intValue();  45.        int y = ((Integer) value).intValue();  46.  47.        if (!isValidDate(d, m, y)) {  48.           FacesMessage message   49.              = com.corejsf.util.Messages.getMessage(  50.                 "com.corejsf.messages", "invalidDate", null);  51.           message.setSeverity(FacesMessage.SEVERITY_ERROR);  52.           throw new ValidatorException(message);  53.         }  54.     }  55.  56.     private static boolean isValidDate(int d, int m, int y) {  57.        if (d < 1 || m < 1 || m > 12) return false;  58.        if (m == 2) {  59.           if (isLeapYear(y)) return d <= 29;  60.           else return d <= 28;  61.        }  62.        else if (m == 4 || m == 6 || m == 9 || m == 11)  63.           return d <= 30;  64.        else   65.           return d <= 31;  66.     }  67.  68.     private static boolean isLeapYear(int y) {  69.        return y % 4 == 0 && (y % 400 == 0 || y % 100 != 0);   70.     }  71. }     

Listing 6-17. validator3/web/index.jsp

  1. <html>   2.    <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>   3.    <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>   4.    <f:view>   5.       <head>   6.          <link href="styles.css" rel="stylesheet" type="text/css"/>   7.          <title><h:outputText value="#{msgs.title}"/></title>   8.       </head>   9.       <body>  10.          <h:form>  11.             <h1><h:outputText value="#{msgs.enterDate}"/></h1>  12.             <h:panelGrid columns="3">  13.                <h:outputText value="#{msgs.day}"/>  14.                <h:inputText value="#{bb.day}" binding="#{bb.dayInput}"   15.                   size="2" required="true"/>  16.                <h:panelGroup/>  17.  18.                <h:outputText value="#{msgs.month}"/>  19.                <h:inputText value="#{bb.month}" binding="#{bb.monthInput}"   20.                   size="2" required="true"/>  21.                <h:panelGroup/>  22.  23.                <h:outputText value="#{msgs.year}"/>  24.                <h:inputText  value="#{bb.year}"   25.                   binding="#{bb.yearInput}" size="4" required="true"   26.                   validator="#{bb.validateDate}"/>  27.                <h:message for="year" style/>  28.             </h:panelGrid>  29.             <h:commandButton value="#{msgs.submit}" action="submit"/>  30.          </h:form>  31.       </body>  32.    </f:view>  33. </html>     

An alternative approach is to attach the validator to a hidden input field that comes after all other fields on the form:

  <h:inputHidden  validator="#{bb.validateDate}"      value="needed"/>

The hidden field is rendered as a hidden HTML input field. When the field value is posted back, the validator kicks in. (It is essential that you supply some field value. Otherwise, the component value is never updated.) With this approach, the validation function is more symmetrical since all other form components already have their local values set.

Note

It would actually be worthwhile to write a custom date component that renders three input fields and has a single value of type Date. That single component could then be validated easily. However, the technique of this section is useful for any form that needs validation across fields.


As you have seen, JSF provides extensive and extensible support for conversion and validation. You can use the JSF standard converter and validators with one line of code in your JSF pages, or you can supply your own logic if more complex conversions or validations are needed. Finally, as you will see in Chapter 9, you can define your own conversion and validation tags.



Core JavaServerT Faces
Core JavaServer(TM) Faces (2nd Edition)
ISBN: 0131738860
EAN: 2147483647
Year: 2004
Pages: 84

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net