Previous Section  < Day Day Up >  Next Section

8.2 Handling Application Backend Events

Separate the queuing from the firing of the event is the easy part. Dealing with the two subcategories of ActionEvent requires one more twist. From what I've said so far, it looks like an ActionEvent is always processed at the end of the Apply Request Values phase, but that's only appropriate for user interface events. As you may recall, an ActionEvent that requires backend processing must not be handled until the model properties are updated, i.e., after the Update Model Values phase at the earliest.

Let's look at some real examples of action handling using the report entry form in the sample application. We start with events that invoke backend logic in this section and look at user interface events in the next.

As you may recall, the report entry form has three fields for entering a date, an expense type and an amount for a report entry, and an Add button. Figure 8-2 shows the form produced by the version of the page we use in this section.

Figure 8-2. The report entry form area with a few entries
figs/Jsf_0802.gif

The Add button is a typical example of the most common type of event handling, namely a button that fires an event that requires processing by the application backend. When the user clicks this button, a new entry with the values entered in the fields is added to the current report.

Example 8-1 shows a version of the JSP page where the Add button is bound to an event handling method that invokes the backend code for adding the entry.

Example 8-1. Entry form area JSP page with an Add button action reference (expense/stage2/entryFormArea.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" %>



<f:view>

  <h:form>

    Title: 

    <h:inputText id="title" size="30" required="true"

      value="#{reportHandler.currentReport.title}" />

    <h:message for="title" />

    <br>

    Entry:

    <br>

    Date: 

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

      value="#{entryHandler.currentEntry.date}">

      <f:convertDateTime dateStyle="short" />

    </h:inputText>

    <h:message for="date" />

    <br>

    Type:

    <h:selectOneMenu id="type" required="true"

      value="#{entryHandler.currentEntry.type}">

      <f:selectItems value="#{entryHandler.expenseTypeChoices}"/>

    </h:selectOneMenu>

    <h:message for="type" />

    <br>

    Amount:

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

      value="#{entryHandler.currentEntry.amount}">

      <f:convertNumber pattern="#,##0.00" />

      <f:validateDoubleRange minimum="1"/>

    </h:inputText>

    <h:message for="amount" />

    <br>

    <h:commandButton value="Add"

      disabled="#{reportHandler.addEntryDisabled}"

      action="#{entryHandler.add}" />

  </h:form>

  <h:messages globalOnly="true" />



  <%-- Loop to verify that it works --%>

  <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

  <ol>

    <c:forEach items="${reportHandler.currentReportEntries}" var="e">

      <li>Date: ${e.date}, Type: ${e.type}, Amount: ${e.amount}</li>

    </c:forEach>

  </ol>

</f:view>

The only real difference compared to the version of the page we used in Chapter 7 is the action attribute for the Add button <h:commandButton> action element. Of less interest at this point is the JSTL <c:forEach> action that lists all entries in the current report at the end of the page. I added the loop just to verify that the Add button's event handler really does what it's supposed to do. This loop gets the report entries from the report handler we looked at in Chapter 6. I'm not showing you the details here, but I promise to return to them when we replace the plain list with a real report entries table in Chapter 10.

The action attribute contains a method binding expression. The method binding expression syntax is similar to that of a value binding expression. It's a subset of the JSF EL that evaluates to a bean method with a certain signature. In Example 8-1, the method binding expression points to a method named add in the EntryHandler instance available through the entryHandler variable. Before we look at the add() method, let's see how it relates to the event listeners and the request processing lifecycle we discussed earlier.

8.2.1 Using an Action Method and the Default ActionListener

The UICommand component supports method bindings for two types of methods: action methods and action listener methods. Either type can be used to process an ActionEvent, but the action method type is the most commonly used. An action method has no parameters and returns a String value called the action outcome. The outcome is typically "success" if everything went as planned, and "error" or "failure" if there was a problem. The outcome value can affect which view is displayed next, which we get back to in Chapter 9. In a JSP page, you can use the action attribute with a method binding expression to bind a UICommand component to an action method, as shown in Example 8-1.

As you know from the earlier discussions, an ActionEvent is handled by a listener that implements the ActionListener interface, so there must be something else going on to invoke the action method. The missing piece is called the default ActionListener. This is a listener that is provided by the JSF implementation[1] to make it easier to handle events. When a UICommand component is asked to fire an ActionEvent, it first notifies all regular listeners attached to the component, if any. Then it checks if it's been configured with an action method binding. If so, it creates an instance of the default ActionListener and asks it to handle the event. The default ActionListener evaluates the action method binding and invokes the method. As hinted at earlier, the default ActionListener uses the action outcome value to decide which view to use for the response. All of this happens behind the scenes, so you just have to write the action method and bind it to the component.

[1] As is true for pretty much everything in JSF, an application can define a customized default ActionListener if needed.

Example 8-2 shows the add() action method bound to the Add button in Example 8-1.

Example 8-2. The EntryHandler add action method
package com.mycompany.expense;

...



public class EntryHandler {

    private ReportHandler reportHandler;

    private ReportEntry currentEntry;

    ...



    public String add( ) {

        return reportHandler.addEntry(currentEntry);

    }

    ...

}

The add() method is very simple: it just relays the call to the ReportHandler, which is in charge of accessing the current report, passing on the current entry as an argument to the addEntry() method.

So the meat is in the ReportHandler addEntry() method, shown in Example 8-3.

Example 8-3. The ReportHandler addEntry( ) method
package com.mycompany.expense;



import javax.faces.application.Application;

import javax.faces.application.MessageResources;

import javax.faces.application.Message;

import javax.faces.context.FacesContext;

...



public class ReportHandler {

    private ReportRegistry registry;

    private Rules rules;

    private Report currentReport;

    private String currentUser;

    private boolean isManager;



    ...

    public String addEntry(ReportEntry entry) {

        try {

            refreshCache( );

        }

        catch (RegistryException e) {

            addMessage("registry_error", e.getMessage( ));

            return "error";

        }



        if (!rules.canEdit(currentUser, isManager, currentReport)) {

            addMessage("report_no_edit_access", null);

            return "error";

        }



        String outcome = "success";

        currentReport.addEntry(entry);

        try {

            saveReport( );

        }

        catch (RegistryException e) {

            currentReport.removeEntry(entry.getId( ));

            addMessage("registry_error", e.getMessage( ));

            outcome = "error";

        }

        return outcome;

    }

The addEntry() method first calls a method named refreshCache( ) to ensure it has the most recent copy of the current report, in case some other user has modified it:

    private void refreshCache( ) throws RegistryException {

        if (!isReportNew( )) {

            setCurrentReport(registry.getReport(currentReport.getId( )));

        }

    }



    private void setCurrentReport(Report report) {

        currentReport = report;

        entriesModel = null;

    }

If the refresh fails, the addEntry() method adds an error message to the JSF message list and returns "error" as the outcome. Next, it checks that the current user is allowed to edit the current report by calling the canEdit() method implemented by the Rules class we looked at in Chapter 6. If the answer is "no", it adds another error message to the list.

If the user has write access, the entry is added to the current report by calling the addEntry() method on the current Report instance and the updated report is saved in the registry by calling saveReport():

    private void saveReport( ) throws RegistryException {

        if (isReportNew( )) {

            currentReport.setStatus(Report.STATUS_OPEN);

            registry.addReport(currentReport);

        }

        else {

            registry.updateReport(currentReport);

        }

        entriesModel = null;

    }

The saveReport() method adds the report to the registry if it's new (i.e., it hasn't been saved yet) or updates the registry version of the report if it's already in the registry.

Saving the report may fail, and if it does, the addEntry( ) method restores the local copy of the current report to its previous state by removing the entry it just added, and adds an error message to the list as before.

All error messages are added to the JSF message list by the addMessage() method:

    private void addMessage(String messageKey, Object param) {

        FacesContext context = FacesContext.getCurrentInstance( );

        Application application = context.getApplication( );

        String messageBundleName = application.getMessageBundle( );

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

        ResourceBundle rb = 

            ResourceBundle.getBundle(messageBundleName, locale);

        String msgPattern = rb.getString(messageKey);

        String msg = msgPattern;

        if (param != null) {

            Object[] params = {param};

            msg = MessageFormat.format(msgPattern, params);

        }

        FacesMessage facesMsg = 

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

        context.addMessage(null, facesMsg);

    }

The only difference compared to the code we used in Chapter 7 is that instead of throwing an exception that contains the FacesMessage instance, and relying on the component to add it to the message queue, I use the FacesContext addMessage() method to queue the message myself. The first addMessage( ) argument takes a component's client ID. This must be used for a message that is associated with a specific component instance. Here I use a null value instead, which means that the message is an application-level message. If you look at Example 8-1, there's an <h:messages> action element at the bottom of the page, just before the entries loop, with a globalOnly attribute set to true. Configured this way, the action displays only application-level error messages. The component-specific messages are displayed by <h:message> actions for each component, just as before.

8.2.2 Using an Action Listener Method or Instance

In most cases, you don't need access to the actual ActionEvent in order to do the right thing, because each component can be bound to a separate action method that implements component-specific behavior. The exception is when one method needs to handle events fired by multiple components, but that turns out to be a very rare case. In fact, there's nothing in the sample application that requires this, and I'm hard pressed to come up with any good use case for it. Even so, the JSF specification supports two alternatives to the action method that provides access to the ActionEvent, in case you do need it.

The first alternative is the action listener method binding, defined by the actionListener attribute in a JSP page:

<h:commandButton value="Add"

  actionListener="#{entryHandler.handleAdd}" />

An action listener method has the exact same signature as the ActionListener processAction() method we looked at earlier; it takes an ActionEvent argument and has a void return type:

    public void processAction(ActionEvent e) throws AbortProcessingException {

        UIComponent myCommand = e.getComponent( );

        // Do whatever you need to do depending on which component fired

        // the event

        ...

    }

Any object available as a JSF EL expression variable can implement the action listener method, but you can bind a component only to one action listener method. If you need to use more than one listener to handle the same event—maybe one that logs the event and another that really acts on it—you can attach as many ActionListener instances as you want to the component. The JSF core tag library includes an action element that makes this easy to do in a JSP page:

<h:commandButton value="Add">

  <f:actionListener type="com.mycompany.LogEventListener" />

  <f:actionListener type="com.mycompany.HandleAddListener" />

</h:commandButton>

The <f:actionListener> action creates an instance of the class specified by the type attribute and calls the addActionListener() method on the UICommand component.

8.2.3 Specifying When to Fire an Event

So far, I've told you how to bind a component to an event handling method, but I haven't let you in yet on the secret of how an ActionEvent can be made to fire either at the end of the Apply Request Value phase or in the Invoke Application phase.

There are different ways to solve this problem, but the specification group decided to go with an approach in which the source component decides when the event should be processed, with a little bit of help from the application developer. Here's how it works. The FacesEvent class—which all JSF events must extend either directly or through one of the standard subclasses, such as ActionEvent—defines a property named phaseId:

package javax.faces.event;



import java.util.EventObject;

...

public abstract class FacesEvent extends EventObject {

    private PhaseId phaseId = PhaseId.ANY_PHASE;



    public PhaseId getPhaseId( ) {

        return phaseId;

    }



    public void setPhaseId(PhaseId phaseId) {

        this.phaseId = phaseId;

    }

    ...

}

The phaseId property data type is PhaseId, which is a type-safe enumeration containing one value per request processing lifecycle phase: PhaseId.APPLY_REQUEST_VALUES, PhaseId.PROCESS_VALIDATIONS, PhaseId.UPDATE_MODEL_VALUES, PhaseId.INVOKE_APPLICATION, PhaseId.RENDER_RESPONSE, or PhaseId.ANY_PHASE. The PhaseId.ANY_PHASE value means "process the event in the phase where it was queued," and it's the default value for the phaseId property.

So even though UICommand always queues an ActionEvent in the Apply Request Values phase, it sets the phaseId to PhaseId.INVOKE_APPLICATION to delay the event handling unless you tell it that you want to process it immediately. You do so through a UICommand property called immediate. If immediate is true, the phaseId is left unchanged so the event is processed in the Apply Request Values phase. The default ActionListener also tells JSF to proceed directly to the Render Response phase after invoking the action method. The default value for immediate is false, because the ActionEvent is typically used to invoke backend code.

Most of the logic for keeping track of the phase in which an event is to be processed is implemented by the UIViewRoot component that sits at the top of the component tree. At the end of each phase, UIViewRoot goes through the event queue and calls the broadcast() method on all event source components. It starts with all events with phaseId set to PhaseId.ANY_PHASE and then the events queued for the current phase. It continues until there are no more events with these phaseId values in the queue, so if processing one event leads to a new event, the new event is also processed, as long as it's for a matching phase.

    Previous Section  < Day Day Up >  Next Section