1.6.3. Understanding Seam conversations

This tutorial concentrates upon one particular piece of functionality: placing a hotel reservation. From the user's perspective, hotel search, selection, booking, and confirmation are one continuous unit of work — a conversation. However, from our perspective, it is important that searching remains separate so that users can select multiple hotels from the same search results page, and open distinct conversations in separate browser tabs.
Most web application architectures do not have first class constructs to represent conversations, which makes managing conversational state problematic. Java web applications generally use a combination of several techniques. Some state is transferred in the URL, but what cannot be transferred here is either added to the HttpSession or recorded to the database at the beginning and end of each request.
Since the database is the least-scalable tier, this drastically reduces scalability. The extra traffic to and from the database also increases latency. In order to reduce redundant traffic, Java applications often introduce a data cache to store commonly-accessed data between requests. However, since invalidation is based upon an LRU policy, rather than whether the user has finished using the data, this cache is inefficient. It is also shared between concurrent transactions, which introduces further issues associated with keeping the cached state consistent with that of the database.
State held in the HttpSession suffers similar issues. The HttpSession is fine for storing true session data — data common to all requests between user and application — but for data related to individual request series, it does not work so well. Conversations stored here quickly break down when dealing with multiple windows or the back button. Without careful programming, data in the HttpSession can also grow quite large, which makes the session difficult to cluster. Developing mechanisms to deal with the problems these methods present (by isolating session state associated with distinct concurrent conversations, and incorporating failsafes to ensure conversation state is destroyed when a conversation is aborted) can be complicated.
Seam greatly improves conditions by introducing conversation context as a first class construct. Conversation state is stored safely in this context, with a well-defined life cycle. Even better, there is no need to push data continually between the application server and the database; the conversation context is a natural cache for currently-used data.
In the following application, the conversation context is used to store stateful session beans. These are sometimes regarded as detrimental to scalability, and in the past, they may have been. However, modern application servers have sophisticated mechanisms for stateful session bean replication. JBoss Enterprise Application Platform performs fine-grained replication, replicating only altered bean attribute values. Used correctly, stateful session beans pose no scalability problems, but for those uncomfortable or unfamiliar with the use of stateful session beans, Seam also allows the use of POJOs.
The booking example shows one way that stateful components with different scopes can collaborate to achieve complex behaviors. The main page of the booking application allows the user to search for hotels. Search results are stored in the Seam session scope. When the user navigate to a hotel, a conversation begins, and a conversation scoped component retrieves the selected hotel from the session scoped component.
The booking example also demonstrates the use of RichFaces Ajax to implement rich client behavior without handwritten JavaScript.
The search function is implemented with a session-scoped stateful session bean, similar to the one used in the message list example.

Example 1.28. HotelSearchingAction.java

@Stateful                                                                                     1
@Name("hotelSearch")
@Scope(ScopeType.SESSION)
@Restrict("#{identity.loggedIn}")                                                             2
public class HotelSearchingAction implements HotelSearching
{
   
    @PersistenceContext
    private EntityManager em;
   
    private String searchString;
    private int pageSize = 10;
    private int page;
   
    @DataModel                                                                                3
    private List<Hotel> hotels;
   
    public void find()
    {
        page = 0;
        queryHotels();
    }
    public void nextPage()
    {
        page++;
        queryHotels();
    }
      
    private void queryHotels()
    {
        hotels = 
            em.createQuery("select h from Hotel h where lower(h.name) like #{pattern} " + 
                           "or lower(h.city) like #{pattern} " + 
                           "or lower(h.zip) like #{pattern} " +
                           "or lower(h.address) like #{pattern}")
              .setMaxResults(pageSize)
              .setFirstResult( page * pageSize )
              .getResultList();
    }
   
    public boolean isNextPageAvailable()
    {
        return hotels!=null && hotels.size()==pageSize;
    }
   
    public int getPageSize() {
        return pageSize;
    }
   
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
   
    @Factory(value="pattern", scope=ScopeType.EVENT)
    public String getSearchPattern()
    {
        return searchString==null ? 
            "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%';
    }
   
    public String getSearchString()
    {
        return searchString;
    }
   
    public void setSearchString(String searchString)
    {
        this.searchString = searchString;
    }
   
    @Remove                                                                                   4
    public void destroy() {}
}

1

The EJB standard @Stateful annotation identifies this class as a stateful session bean. Stateful session beans are scoped to the conversation context by default.

2

The @Restrict annotation applies a security restriction to the component. It restricts access to the component allowing only logged-in users. The security chapter explains more about security in Seam.

3

The @DataModel annotation exposes a List as a JSF ListDataModel. This makes it easy to implement clickable lists for search screens. In this case, the list of hotels is exposed to the page as a ListDataModel in the conversation variable named hotels.

4

The EJB standard @Remove annotation specifies that a stateful session bean should be removed and its state destroyed after invocation of the annotated method. In Seam, all stateful session beans must define a parameterless method marked @Remove. This method will be called when Seam destroys the session context.
The main page of the application is a Facelets page. The fragment that relates to searching for hotels is shown below:

Example 1.29. main.xhtml

<div class="section">
  
  <span class="errors">
    <h:messages globalOnly="true"/>
  </span>
  
  <h1>Search Hotels</h1>

  <h:form id="searchCriteria">
    <fieldset> 
      <h:inputText id="searchString" value="#{hotelSearch.searchString}" 
                   style="width: 165px;">
        <a:support event="onkeyup" actionListener="#{hotelSearch.find}"                     1
                   reRender="searchResults" />
      </h:inputText>
      &#160;
      <a:commandButton id="findHotels" value="Find Hotels" action="#{hotelSearch.find}" 
                       reRender="searchResults"/>
      &#160;
      <a:status>                                                                            2
        <f:facet name="start">
          <h:graphicImage value="/img/spinner.gif"/>
        </f:facet>
      </a:status>
      <br/>
      <h:outputLabel for="pageSize">Maximum results:</h:outputLabel>&#160;
      <h:selectOneMenu value="#{hotelSearch.pageSize}" id="pageSize">
        <f:selectItem itemLabel="5" itemValue="5"/>
        <f:selectItem itemLabel="10" itemValue="10"/>
        <f:selectItem itemLabel="20" itemValue="20"/>
      </h:selectOneMenu>
    </fieldset>
  </h:form>
  
</div>

<a:outputPanel id="searchResults">                                                          3
  <div class="section">
    <h:outputText value="No Hotels Found"
                  rendered="#{hotels != null and hotels.rowCount==0}"/>
    <h:dataTable id="hotels" value="#{hotels}" var="hot" 
                 rendered="#{hotels.rowCount>0}">
      <h:column>
        <f:facet name="header">Name</f:facet>
        #{hot.name}
      </h:column>
      <h:column>
        <f:facet name="header">Address</f:facet>
        #{hot.address}
      </h:column>
      <h:column>
        <f:facet name="header">City, State</f:facet>
        #{hot.city}, #{hot.state}, #{hot.country}
      </h:column> 
      <h:column>
        <f:facet name="header">Zip</f:facet>
        #{hot.zip}
      </h:column>
      <h:column>
        <f:facet name="header">Action</f:facet>
        <s:link id="viewHotel" value="View Hotel"                                           4
                action="#{hotelBooking.selectHotel(hot)}"/>
      </h:column>
    </h:dataTable>
    <s:link value="More results" action="#{hotelSearch.nextPage}" 
            rendered="#{hotelSearch.nextPageAvailable}"/>
  </div>
</a:outputPanel>

1

The RichFaces Ajax <a:support> tag allows a JSF action event listener to be called by asynchronous XMLHttpRequest when a JavaScript event like onkeyup occurs. Even better, the reRender attribute lets us render a fragment of the JSF page and perform a partial page update when the asynchronous response is received.

2

The RichFaces Ajax <a:status> tag lets us display an animated image while we wait for asynchronous requests to return.

3

The RichFaces Ajax <a:outputPanel> tag defines a region of the page which can be re-rendered by an asynchronous request.

4

The Seam <s:link> tag lets us attach a JSF action listener to an ordinary (non-JavaScript) HTML link. The advantage of this over the standard JSF <h:commandLink> is that it preserves the operation of "open in new window" and "open in new tab". Also notice that we use a method binding with a parameter: #{hotelBooking.selectHotel(hot)}. This is not possible in the standard Unified EL, but Seam provides an extension to the EL that lets you use parameters on any method binding expression.
If you are wondering how navigation occurs, you can find all the rules in WEB-INF/pages.xml; this is discussed in Section 7.7, “Navigation”.
This page displays search results dynamically as the user types, and passes a selected hotel to the selectHotel() method of HotelBookingAction, where the real work occurs.
The following code shows how the booking example application uses a conversation-scoped stateful session bean to achieve a natural cache of persistent data related to the conversation. Think of the code as a list of scripted actions that implement the various steps of the conversation.

Example 1.30. HotelBookingAction.java

@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking
{
   
    @PersistenceContext(type=EXTENDED)                                                1
    private EntityManager em;
   
    @In 
        private User user;
   
    @In(required=false) @Out
    private Hotel hotel;
   
    @In(required=false) 
    @Out(required=false)                                                              2
    private Booking booking;
     
    @In
    private FacesMessages facesMessages;
      
    @In
    private Events events;
   
    @Logger 
        private Log log;
   
    private boolean bookingValid;
   
    @Begin                                                                            3
    public void selectHotel(Hotel selectedHotel)
    {
        hotel = em.merge(selectedHotel);
    }
   
    public void bookHotel()
    {      
        booking = new Booking(hotel, user);
        Calendar calendar = Calendar.getInstance();
        booking.setCheckinDate( calendar.getTime() );
        calendar.add(Calendar.DAY_OF_MONTH, 1);
        booking.setCheckoutDate( calendar.getTime() );
    }
   
    public void setBookingDetails()
    {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_MONTH, -1);
        if ( booking.getCheckinDate().before( calendar.getTime() ) )
            {
                facesMessages.addToControl("checkinDate", 
                                           "Check in date must be a future date");
                bookingValid=false;
            }
        else if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) )
            {
                facesMessages.addToControl("checkoutDate", 
                                           "Check out date must be later " + 
                                           "than check in date");
                bookingValid=false;
            }
        else
            {
                bookingValid=true;
            }
    }
   
    public boolean isBookingValid()
    {
        return bookingValid;
    }
   
    @End                                                                              4
    public void confirm()
    {
        em.persist(booking);
        facesMessages.add("Thank you, #{user.name}, your confimation number " + 
                          " for #{hotel.name} is #{booki g.id}");
        log.info("New booking: #{booking.id} for #{user.username}");
        events.raiseTransactionSuccessEvent("bookingConfirmed");
    }
   
    @End
    public void cancel() {}
   
    @Remove                                                                           5
    public void destroy() {}
}

1

This bean uses an EJB3 extended persistence context, so that any entity instances remain managed for the whole life cycle of the stateful session bean.

2

The @Out annotation declares that an attribute value is outjected to a context variable after method invocations. In this case, the context variable named hotel will be set to the value of the hotel instance variable after every action listener invocation completes.

3

The @Begin annotation specifies that the annotated method begins a long-running conversation, so the current conversation context will not be destroyed at the end of the request. Instead, it will be reassociated with every request from the current window, and destroyed either by timeout due to conversation inactivity or invocation of a matching @End method.

4

The @End annotation specifies that the annotated method ends the current long-running conversation, so the current conversation context will be destroyed at the end of the request.

5

This EJB remove method will be called when Seam destroys the conversation context. Do not forget to define this method!
HotelBookingAction contains all the action listener methods that implement selection, booking and booking confirmation, and holds state related to this work in its instance variables. This code is much cleaner and simpler than getting and setting HttpSession attributes.
Even better, a user can have multiple isolated conversations per log in session. Log in, run a search, and navigate to different hotel pages in multiple browser tabs. You'll be able to work on creating two different hotel reservations at the same time. If you leave any one conversation inactive for long enough, Seam will eventually time out that conversation and destroy its state. If, after ending a conversation, you backtrack to a page of that conversation and try to perform an action, Seam will detect that the conversation was already ended, and redirect you to the search page.