Previous Section  < Day Day Up >  Next Section

7.4 Using a Custom Validator

The standard validators are sufficient for many applications, but sometimes you have to roll your own to get what you need. The filtering criteria form in the sample application, for instance, has two date fields for specifying a date range. With a custom validator, we can make sure that the date entered in the To field is in fact later than the date entered in the From field, so let's build one. Figure 7-2 shows the filtering criteria form with a validation message from the custom validator.

Figure 7-2. The filtering criteria form with a custom validator
figs/Jsf_0702.gif

Example 7-3 shows a custom validator class that compares the date values of two components to ensure that one is later than the other.

Example 7-3. The LaterThanValidator class
package com.mycompany.jsf.validator;



import java.text.MessageFormat;

import java.io.Serializable;

import java.util.Date;

import java.util.Locale;

import java.util.ResourceBundle;

import javax.faces.application.Application;

import javax.faces.application.FacesMessage;

import javax.faces.component.UIComponent;

import javax.faces.component.EditableValueHolder;

import javax.faces.component.ValueHolder;

import javax.faces.context.FacesContext;

import javax.faces.convert.Converter;

import javax.faces.validator.Validator;

import javax.faces.validator.ValidatorException;



public class LaterThanValidator implements Validator, Serializable {

    private String peerId;



    public void setPeerId(String peerId) {

        this.peerId = peerId;

    }



    public void validate(FacesContext context, UIComponent component,

        Object value) throws ValidatorException {



        Application application = context.getApplication( );



        String messageBundleName = application.getMessageBundle( );

        Locale locale = context.getViewRoot( ).getLocale( );

        ResourceBundle rb = 

            ResourceBundle.getBundle(messageBundleName, locale);



        UIComponent peerComponent = component.findComponent(peerId);

        if (peerComponent == null) {

            String msg = rb.getString("peer_not_found");

            FacesMessage facesMsg = 

                new FacesMessage(FacesMessage.SEVERITY_FATAL, msg, msg);

            throw new ValidatorException(facesMsg);

        }

        

        ValueHolder peer = null;

        try {

            peer = (ValueHolder) peerComponent;

        }

        catch (ClassCastException e) {

            String msg = rb.getString("invalid_component");

            FacesMessage facesMsg = 

                new FacesMessage(FacesMessage.SEVERITY_FATAL, msg, msg);

            throw new ValidatorException(facesMsg);

        }



        if (peer instanceof EditableValueHolder &&

            !((EditableValueHolder) peer).isValid( )) {

            // No point in validating against an invalid value

            return;

        }



        Object peerValue = peer.getValue( );

        if (!(peerValue instanceof Date)) {

            String msg = rb.getString("invalid_peer");

            FacesMessage facesMsg = 

                new FacesMessage(FacesMessage.SEVERITY_FATAL, msg, msg);

            throw new ValidatorException(facesMsg);

        }



        if (!(value instanceof Date) || 

            !((Date) value).after((Date) peerValue)) {

            String msg = rb.getString("not_later");

            String detailMsgPattern = rb.getString("not_later_detail");

            Object[] params = 

                {formatDate((Date) peerValue, context, component)};

            String detailMsg = 

                MessageFormat.format(detailedMsgPattern, params);

            FacesMessage facesMsg = 

                new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, detailMsg);

            throw new ValidatorException(facesMsg);

        }

    }

    ...

A validator implements the javax.faces.validator.Validator interface, which defines a single method named validate(). Regular JavaBeans property setter methods are often used to configure a validator. The LaterThanValidator needs to know about two values: the value to be validated and the value to compare it to. JSF passes the LaterThanValidator the value to validate as an argument to validate(), and a property named peerId holds a reference to the component with the value to compare it to.

The validate() method is where all the real work takes place. First, it needs to get the message bundle that holds the messages for this validator. The bundle name specified by the <message-bundle> element in the faces-config.xml file is returned by the getMessageBundle() method of the Application class. The LaterThanValidator requires that all its messages be defined in this bundle, but you may want to provide default messages in a separate bundle bundled with the validator if you're developing validators to be used with many applications. If so, you should still look in the application's bundle first. Only use the default bundle if the message isn't defined in the application bundle. That's how all the JSF standard validators work.

Next, the LaterThanValidator tries to find the component with the value to compare to, defined by the peerId property. It does this by calling the findComponent() method on the component the validator is attached to. If it can't find the peer component, it gets the message with the key peer_not_found from the message bundle. For a reusable validator, you should use message keys that stand a chance of being unique in any application. Instead of using a short name like peer_not_found, include the same type of reversed domain name as is recommended for Java package names as a key prefix, e.g., com.mycompany.peer_not_found. If you're working for a large company, you may want to add additional parts to the prefix to avoid name clashes with messages created in other parts of the company.

The validate() method creates a javax.faces.application.FacesMessage instance to hold the message text. The FacesMessage class defines three properties: severity, summary, and detail. All of them can be set as constructor arguments, as they are here, or they can be set through setter methods. The severity must be set to one of four FacesMessage constants: SEVERITY_INFO, SEVERITY_WARN, SEVERITY_ERROR, or SEVERITY_FATAL. You can use <h:message> and <h:messages> attributes to ask for different style sheet classes to be used for messages of different severity, e.g., to render informational messages black and error messages red. For all LaterThanValidator messages that relate to errors a developer may make (i.e., an invalid peer component), I use the fatal severity level and the same text for both the summary and detail. I use the error severity for the error a user may make, i.e., enter a date that is not later than the first date.

To signal that the validation failed, the validator throws a javax.faces.validator.ValidatorException that contains the FacesMessage instance. The component that calls the validate() method catches the exception and adds it to the message queue so that it can be displayed to the user by a <h:message> or <h:messages> action when the response is rendered. It also marks the component value as invalid.

If the peer component is found, the validate() method tries to cast the component reference to the javax.faces.component.ValueHolder type. This is an interface implemented by all component types that hold a value, containing all methods for accessing the component value. With a few exceptions, most standard component types implement this interface. If the peer component reference can't be cast to this type, an exception holding a message with the invalid_component text is thrown, just as it was before.

The EditableValueHolder interface extends the ValueHolder interface, and all component types that let the user change the value it holds implement it (for example, the UIInput component). If the user can change the value, it's possible that she entered an incorrect value. The LaterThanValidator therefore checks if the peer component is marked as invalid, and stops the processing if it is. It doesn't throw an exception in this case, because there's already a message queued for the peer component that describes why it's invalid.

Next up is a test on the data type of the peer component value. If it's not a java.util.Date, an exception holding a message with the invalid_peer text is thrown.

Finally, the real test can be done: is the value of the component the validator is attached to a Date that's later than the Date value of the peer component? If the answer is "no," an exception holding a message with the not_later text is thrown. Compared to the other messages, this one is slightly different. Note how I get separate detail text from the bundle and use java.text.MessageFormat to format it. The not_later text contains a placeholder, as you will soon see, and formatting the text replaces it with the peer component's value.

Before replacing the placeholder in the detail text, I use the private formatDate() method to convert the peer component's Date value to a formatted String:

    private String formatDate(Date date, FacesContext context, 

        UIComponent component) {

        Converter converter = ((ValueHolder) component).getConverter( );

        if (converter != null) {

            return converter.getAsString(context, component, date);

        }

        return date.toString( );

    }

}

The formatDate() method tries to get the converter the component is equipped with. If it finds one, it uses it to convert the Date object; otherwise, it uses the toString() method. Because this method is called to format the value shown in the error message, the value in the message is always formatted the way the component wants it, no matter how the converter is configured.

That's the code. Now we must create the messages. We created the application's message bundle properties file earlier (the custMessage.properties file), so all we have to do is add the keys and text that the LaterThanValidator uses:

invalid_component=The component to compare to is of a type that can't have a value

peer_not_found=The value to compare to can't be found

invalid_peer=Can only compare to a date value

not_later=Invalid date

not_later_detail=Please enter a date later than {0}

There's really nothing new here. The messages for the custom validator are declared the same way as the replaced standard message we looked at earlier was.

7.4.1 Registering a Custom Validator

Okay, so now we have a custom validator and all its messages are defined in the custMessage.properties file. How can we use it? In Java code, you could simply create an instance of the validator class, set the peerId property, and attach the validator to a component, like this:

import com.mycompany.jsf.validator.LaterThanValidator;

import javax.faces.component.UIInput;



    UIInput from = new UIInput( );

    from.setId("from");

    UIInput to = new UIInput( );

    LaterThanValidator validator = new LaterThanValidator( );

    validator.setPeerId("from");

    to.addValidator(validator);

    ...

But, as I mentioned earlier, JSF allows you to register pretty much every type of class under a symbolic name, so that you can replace implementations without changing all the code that uses it. Validators are no exception, and here's how you register the LaterThanValidator in the faces-config.xml file:

<faces-config>

  ...

  <validator>

    <validator-id>com.mycompany.jsf.validator.LATER_THAN</validator-id>

    <validator-class>

      com.mycompany.jsf.validator.LaterThanValidator

    </validator-class>

    <property>

      <property-name>peerId</property-name>

      <property-class>java.lang.String</property-class>

    </property>

  </validator>

  ...

</faces-config>

A top-level <validator> element contains a <validator-id> element with a unique identifier and a <validator-class> element with the fully qualified class name. The <property> element, with nested <property-name> and <property-class> elements, declares a supported property. A JSF development tool can use this information to help the user configure the validator.

With this declaration in place, the Java code can be changed (as shown in Example 7-4) to create the validator instance using the ID instead of creating an instance of a specific class.

Example 7-4. Getting a registered validator
import com.mycompany.jsf.validator.LaterThanValidator;

import javax.faces.application.Application;

import javax.faces.component.UIInput;

import javax.faces.context.FacesContext;



    UIInput from = new UIInput( );

    from.setId("from");

    UIInput to = new UIInput( );

    Application application = context.getApplication( );

    LaterThanValidator validator = (LaterThanValidator)

        application.createValidator("com.mycompany.jsf.validator.LATER_THAN");

    validator.setPeerId("from");

    to.addValidator(validator);

    ...

The call to the Application createValidator() method returns an instance of the validator class registered with the specified ID. In order to set the peerId property, I need to cast it to LaterThanValidator; in this example, the registered class must be either this class or a subclass. If a validator doesn't have any properties, any class that implements the Validator interface will do.

7.4.2 Using a Custom Validator in a JSP Page

To use a custom validator in a JSP page, you need a JSP custom action that configures the converter and attaches it to a component. Example 7-5 shows the version of the filtering criteria form from the JSP page that produces the screen in Figure 7-2.

Example 7-5. The filtering criteria form with a custom validator (expense/stage4/filterArea.jsp)
<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<%@ taglib uri="http://mycompany.com/jsftaglib" prefix="my" %>



<f:view>

  <h:form>

    From: 

    <h:inputText id="from" size="8" required="true"

      value="#{reportHandler.from}">

      <f:convertDateTime dateStyle="short" />

    </h:inputText>

    <h:message for="from" />

    <br>

    To: 

    <h:inputText id="to" size="8" required="true"

      value="#{reportHandler.to}">

      <f:convertDateTime dateStyle="short" />

      <my:validateLater than="from" />

    </h:inputText>

    <h:message for="to" />

    <br>

    Status:

    <h:selectManyCheckbox value="#{reportHandler.status}">

      <f:selectItem itemValue="1" itemLabel="Open" />

      <f:selectItem itemValue="2" itemLabel="Submitted" />

      <f:selectItem itemValue="3" itemLabel="Accepted" />

      <f:selectItem itemValue="4" itemLabel="Rejected" />

    </h:selectManyCheckbox>

    <p>

    <h:commandButton value="Filter" />

  </h:form>

</f:view>

The custom action that configures the validator and attaches it to the component for the To field in Example 7-5 is called <my:validateLater> and belongs to the custom tag library declared by the taglib directive with the uri attribute set to http://mycompany.com/jsftaglib at the beginning of the page.

7.4.3 Developing a Validator Custom Action

If you're new to JSP, the concept of custom tag libraries and custom actions may seem like hocus pocus, so let's take it step by step.

You've already seen the JSF and JSTL tag libraries in action. The only thing that differs between them and a custom tag library developed in-house or by a third party is that they are defined by public specifications. Other than that, they are just tag libraries, developed using the same JSP API that you use to develop your own custom tag libraries.

The behavior of a custom action is implemented by a Java class called a tag handler, and linked to a tag name and rules for how it can be used declared in a Tag Library Descriptor (TLD). Related custom actions are declared in the same TLD, together making up the library. Besides custom action declarations, the TLD also contains information about the library itself. Example 7-6 shows the TLD for the library with the <my:validateLater> action.

Example 7-6. The TLD for the example tag library (taglib.tld)
<?xml version="1.0" encoding="ISO-8859-1" ?>

<!DOCTYPE taglib

  PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"

  "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">



<taglib>

  <tlib-version>1.0</tlib-version>

  <jsp-version>1.2</jsp-version>

  <short-name>my</short-name>

  <uri>http://mycompany.com/jsftaglib</uri>



  <tag>

    <name>validateLater</name>

    <tag-class>com.mycompany.jsf.taglib.ValidateLaterThanTag</tag-class>

    <body-content>empty</body-content>



    <attribute>

      <name>than</name>

      <required>true</required>

      <rtexprvalue>false</rtexprvalue>

    </attribute>

  </tag>

</taglib>

The file must use the extension .tld to be recognized as a TLD and it is typically named taglib.tld.

At the beginning of the file, the <tlib-version> element specifies the tag library version, the <jsp-version> element identifies the version of the JSP specification the library complies with, and the <short-name> element provides a suggested namespace prefix for the custom action elements. The most interesting element at this level is the <uri> element. It declares the unique identifier for the library that's used as the uri attribute value of the taglib directive in JSP pages using the library. If you look at Example 7-5, you see how the uri attribute value used for the custom tag library matches the <uri> element value in Example 7-6.

Each custom action in the library is declared by a <tag> element. The nested <name> element gives the element name to use in the JSP page, the <tag-class> element identifies the tag handler Java class, and the <body-content> element declares how the element body should be processed. The empty value used here means that this action element must not have a body at all.

If the custom action supports any attributes, they must be declared by <attribute> elements. The attribute name is given by the <name> element, and the <required> element tells if the attribute is required or not. The <rtexprvalue> element value must be true if the attribute value can be set by a request-time attribute value, e.g., by an JSP EL expression. If it's false, only a static text value can be used as the attribute value. You should always disable request-time attribute values for JSF custom actions, to avoid potential clashes between the JSF EL values and JSP expression values.

The container locates the TLD based on the taglib directive's uri attribute when the JSP page is processed and then uses the information in the TLD to match custom action elements in the JSP page to the corresponding tag handler classes.

The tag handler class for the <my:validate_later> custom action is shown in Example 7-7.

Example 7-7. Validator tag handler class
package com.mycompany.jsf.taglib;



import com.mycompany.jsf.validator.LaterThanValidator;

import javax.faces.application.Application;

import javax.faces.context.FacesContext;

import javax.faces.validator.Validator;

import javax.faces.webapp.ValidatorTag;



public class ValidateLaterThanTag extends ValidatorTag {

    private String peerId;



    public void setThan(String peerId) {

        this.peerId = peerId;

    }



    protected Validator createValidator( ) {

        Application application = 

            FacesContext.getCurrentInstance( ).getApplication( );

        LaterThanValidator validator = (LaterThanValidator)

            application.createValidator("com.mycompany.jsf.validator.LATER_THAN");

        validator.setPeerId(peerId);

        return validator;

    }

}

Because JSF provides a base class, javax.faces.webapp.ValidatorTag, it's very easy to develop this sort of tag handler. The ValidateLaterThanTag class extends the JSF base class and implements just two methods: the setThan( ) method called by JSP with the than action element attribute value, and the createValidator( ) method called by the base class to get the configured validator.

All we have to do in the createValidator() method is get the validator instance from the Application and set its peerId to the than value, which is almost identical to the code in Example 7-4.

7.4.4 Deploying the Custom Tag Library

You can deploy the custom tag library two different ways: you can deploy it as separate files in the web application structure, or package all files in a JAR file and deploy the JAR file. During development, it's often convenient to deploy the files separately, because it makes it easier to replace updated class files or the TLD. Just place the TLD somewhere under the application's WEB-INF directory, for example, in a subdirectory named WEB-INF/tlds. Put the tag handler class files under WEB-INF/classes using a directory structure that matches the package structure, such as WEB-INF/classes/com/mycompany/jsf/taglib for the ValidateLaterThanTag in Example 7-7.

For production deployment, it's better to package all tag library files in a JAR file. The TLD goes in the JAR file's META-INF directory, and the class files in a directory structure mirroring the package structure: for example, META-INF/taglib.tld and com/mycompany/jsf/taglib/ValidateLaterThanTag.class for the example tag library. To deploy the library, put the JAR file in the application's WEB-INF/lib directory.

7.4.5 An Alternative for the Lazy

Creating a special custom action for your custom validators makes it easier for a page author to use them, but JSF actually provides a generic custom action that you can use to attach any validator to a component. It's named <f:validator>:

    <h:inputText id="to" size="8" required="true"

      value="#{reportHandler.to}">

      <f:convertDateTime dateStyle="short" />

      <f:validator validatorId=" com.mycompany.jsf.validator.LATER_THAN" />

      <f:attribute name="peerId" value="from" />

    </h:inputText>

The validatorId attribute value is the identifier the validator is registered with in the faces-config.xml file.

Because the generic <f:validator> action doesn't allow you to set validator-specific properties, you must provide the peer component ID through a generic component attribute instead, set by the <f:attribute> action. This means we must also modify the LaterThanValidator code slightly to get the peer ID from the generic attribute instead. This code in the validate() method replaces the setPeerId() method to deal with this difference:

...

    public void validate(FacesContext context, UIComponent component,

        Object value) throws ValidatorException {



    // Get the peer component ID

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

    ...

I recommend that you take the time to create a custom action, though. It's well worth the five minutes or so that it takes.

    Previous Section  < Day Day Up >  Next Section