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 simply strips out all characters that are not digits. It then creates an object of type CreditCard. If an error was found, then we generate a FacesMessage object and throw a ConverterException. We will discuss these steps in the next section.

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-5 shows the most common credit card formats.

Table 6-5. 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 trivial; 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/WEB-INF/classes/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.       StringBuffer buffer = new StringBuffer(newValue); 13.       boolean foundInvalidCharacter = false; 14.       char invalidCharacter = '\0'; 15.       int i = 0; 16.       while (i < buffer.length() && !foundInvalidCharacter) { 17.          char ch = buffer.charAt(i); 18.          if (Character.isDigit(ch)) 19.             i++; 20.          else if (Character.isWhitespace(ch)) 21.             buffer.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(buffer.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.       StringBuffer result = new StringBuffer(); 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/WEB-INF/classes/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. } 

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> 

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, 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 binding 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 don't 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!

 

graphics/api_icon.gif

 

 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.

 

graphics/api_icon.gif

 

 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.

 

graphics/api_icon.gif

 

 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 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 to 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 on page 213 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. Primitive type values need to be wrapped into objects of the appropriate wrapper classes, such as new Character(invalidCharacter). (For more information about the MessageFormat class, see Horstmann & Cornell, Core Java 5th ed. vol. 2, ch. 10, Sun Microsystems Press 2002.)

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 doesn't contain placeholders.

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

NOTE

graphics/note_icon.gif

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

 

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


 

graphics/api_icon.gif

 

 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.

 

graphics/api_icon.gif

 

 javax.faces.component.UIViewRoot 

  • Locale getLocale()

    Gets the locale for rendering this view.

Listing 6-8. converter2/WEB-INF/classes/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.       if (resource == null) { 53.          bundle = ResourceBundle.getBundle(bundle2, locale, loader); 54.          if (bundle != null) 55.             try { 56.                resource = bundle.getString(resourceId); 57.             } catch (MissingResourceException ex) { 58.             } 59.       } 60. 61.       if (resource == null) return null; // no match 62.       if (params == null) return resource; 63. 64.       MessageFormat formatter = new MessageFormat(resource, locale); 65.       return formatter.format(params); 66.    } 67. 68.    public static Locale getLocale(FacesContext context) { 69.       Locale locale = null; 70.       UIViewRoot viewRoot = context.getViewRoot(); 71.       if (viewRoot != null) locale = viewRoot.getLocale(); 72.       if (locale == null) locale = Locale.getDefault(); 73.       return locale; 74.    } 75. 76.    public static ClassLoader getClassLoader() { 77.       ClassLoader loader = Thread.currentThread().getContextClassLoader(); 78.       if (loader == null) loader = ClassLoader.getSystemClassLoader(); 79.       return loader; 80.    } 81. } 

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.) The custom converter is defined in faces-config.xml (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/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.          <f:loadBundle basename="com.corejsf.messages" var="msgs"/>  8.          <title><h:outputText value="#{msgs.title}"/></title>  9.       </head> 10.       <body> 11.          <h:form> 12.             <h1><h:outputText value="#{msgs.enterPayment}"/></h1> 13.             <h:panelGrid columns="3"> 14.                <h:outputText value="#{msgs.amount}"/> 15.                <h:inputText  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  value="#{payment.card}"> 22.                   <f:converter converter/> 23.                </h:inputText> 24.                <h:message for="card" style/> 25. 26.                <h:outputText value="#{msgs.expirationDate}"/> 27.                <h:inputText  value="#{payment.date}"> 28.                   <f:convertDateTime pattern="MM/yyyy"/> 29.                </h:inputText> 30.                <h:message for="date" style/> 31.             </h:panelGrid> 32.             <h:commandButton value="Process" action="process"/> 33.          </h:form> 34.       </body> 35.    </f:view> 36. </html> 

Listing 6-10. converter2/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.          <f:loadBundle basename="com.corejsf.messages" var="msgs"/>  8.          <title><h:outputText value="#{msgs.title}"/></title>  9.       </head> 10.       <body> 11.          <h:form> 12.             <h1><h:outputText value="#{msgs.paymentInformation}"/></h1> 13.             <h:panelGrid columns="2"> 14.                <h:outputText value="#{msgs.amount}"/> 15.                <h:outputText value="#{payment.amount}"> 16.                   <f:convertNumber type="currency"/> 17.                </h:outputText> 18. 19.                <h:outputText value="#{msgs.creditCard}"/> 20.                <h:outputText value="#{payment.card}" 21.                   converter="com.corejsf.CreditCard"/> 22. 23.                <h:outputText value="#{msgs.expirationDate}"/> 24.                <h:outputText value="#{payment.date}"> 25.                   <f:convertDateTime pattern="MM/yyyy"/> 26.                </h:outputText> 27.             </h:panelGrid> 28.             <h:commandButton value="Back" action="back"/> 29.          </h:form> 30.       </body> 31.    </f:view> 32. </html> 

Listing 6-11. converter2/WEB-INF/faces-config.xml
  1. <?xml version="1.0"?>  2.  3. <!DOCTYPE faces-config PUBLIC  4.   "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"  5.   "http://java.sun.com/dtd/web-facesconfig_1_0.dtd">  6.  7. <faces-config>  8.    <application>  9.       <message-bundle>com.corejsf.messages</message-bundle> 10.    </application> 11. 12.    <navigation-rule> 13.       <from-view-id>/index.jsp</from-view-id> 14.       <navigation-case> 15.          <from-outcome>process</from-outcome> 16.          <to-view-id>/result.jsp</to-view-id> 17.       </navigation-case> 18.    </navigation-rule> 19. 20.    <navigation-rule> 21.       <from-view-id>/result.jsp</from-view-id> 22.       <navigation-case> 23.          <from-outcome>back</from-outcome> 24.          <to-view-id>/index.jsp</to-view-id> 25.       </navigation-case> 26.    </navigation-rule> 27. 28.    <converter> 29.       <converter-id>com.corejsf.CreditCard</converter-id> 30.     <converter-class>com.corejsf.CreditCardConverter</converter-class> 31.    </converter> 32. 33.    <managed-bean> 34.       <managed-bean-name>payment</managed-bean-name> 35.       <managed-bean-class>com.corejsf.PaymentBean</managed-bean-class> 36.       <managed-bean-scope>session</managed-bean-scope> 37.    </managed-bean> 38. </faces-config> 

Listing 6-12. converter2/WEB-INF/classes/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/WEB-INF/classes/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. } 

Figure 6-8. Directory Structure of the Custom Converter Example

graphics/06fig08.jpg


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) 

If validation fails, simply 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 completely 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 on page 229, we use the convenience class com.corejsf.util.Messages to locate the message strings in a resource bundle.

Figure 6-9. Luhn Check Failed

graphics/06fig09.jpg


NOTE

graphics/note_icon.gif

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.


 

graphics/api_icon.gif

 

 javax.faces.validator.Validator 

  • void validate(FacesContext context, UIComponent component)

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

Listing 6-14. Cvalidator2/WEB-INF/classes/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.       StringBuffer digitsOnly = new StringBuffer (); 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've 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

graphics/note_icon.gif

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


NOTE

graphics/note_icon.gif

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.

Listing 6-15. validator2/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.          <f:loadBundle basename="com.corejsf.messages" var="msgs"/>  8.          <title><h:outputText value="#{msgs.title}"/></title>  9.       </head> 10.       <body> 11.          <h:form> 12.             <h1><h:outputText value="#{msgs.enterPayment}"/></h1> 13.             <h:panelGrid columns="3"> 14.                <h:outputText value="#{msgs.amount}"/> 15.                <h:inputText  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  value="#{payment.card}" required="true"> 22.                   <f:converter converter/> 23.                   <f:validator validator/> 24.                </h:inputText> 25.                <h:message for="card" style/> 26. 27.                <h:outputText value="#{msgs.expirationDate}"/> 28.                <h:inputText  value="#{payment.date}"> 29.                   <f:convertDateTime pattern="MM/yyyy"/> 30.                </h:inputText> 31.                <h:message for="date" style/> 32.             </h:panelGrid> 33.             <h:commandButton value="Process" action="process"/> 34.          </h:form> 35.       </body> 36.    </f:view> 37. </html> 

Figure 6-10. The Directory Structure of the Luhn Check Example

graphics/06fig10.jpg


The f:validator tag is useful for simple validators that don't 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 see how to do that later in this chapter.

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 reference, 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.

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

graphics/caution_icon.gif

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


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 textfield. Instead, you would use three different textfields, for the day, month, and year, as in Figure 6-11.

Figure 6-11. Validating a Relationship Involving Three Components

graphics/06fig11.jpg


If the user enters an illegal date, such as February 30, you would like 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 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). Simply 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 since 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.

Listing 6-16. validator3/WEB-INF/classes/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/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.          <f:loadBundle basename="com.corejsf.messages" var="msgs"/>  8.          <title><h:outputText value="#{msgs.title}"/></title>  9.       </head> 10.       <body> 11.          <h:form> 12.             <h1><h:outputText value="#{msgs.enterDate}"/></h1> 13.             <h:panelGrid columns="3"> 14.                <h:outputText value="#{msgs.day}"/> 15.                <h:inputText value="#{bb.day}" binding="#{bb.dayInput}" 16.                   size="2" required="true"/> 17.                <h:panelGroup/> 18. 19.                <h:outputText value="#{msgs.month}"/> 20.                <h:inputText value="#{bb.month}" binding="#{bb.monthInput}" 21.                   size="2" required="true"/> 22.                <h:panelGroup/> 23. 24.                <h:outputText value="#{msgs.year}"/> 25.                <h:inputText  value="#{bb.year}" 26.                   binding="#{bb.yearInput}" size="4" required="true" 27.                   validator="#{bb.validateDate}"/> 28.                <h:message for="year" style/> 29.             </h:panelGrid> 30.             <h:commandButton value="#{msgs.submit}" action="submit"/> 31.          </h:form> 32.       </body> 33.    </f:view> 34. </html> 

Figure 6-12. Directory Structure of the Date Validation Example

graphics/06fig12.jpg


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

graphics/note_icon.gif

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.




core JavaServer Faces
Core JavaServer Faces
ISBN: 0131463055
EAN: 2147483647
Year: 2003
Pages: 121

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