3.4. A complete Seam application: the Hotel Booking example

3.4.1. Introduction

The booking application is a complete hotel room reservation system incorporating the following features:
  • User registration
  • Login
  • Logout
  • Set password
  • Hotel search
  • Hotel selection
  • Room reservation
  • Reservation confirmation
  • Existing reservation list
Booking example
The booking application uses JSF, EJB 3.1, and Seam, along with Facelets for the view. There is also a port of this application to JSF, Facelets, Seam, JavaBeans, and Hibernate 4.
This application is extremely robust. You can open multiple windows, use the back, browser, and refresh buttons, and enter nonsensical data, but it is difficult to crash the application. Seam is designed to build straightforward robust web applications. Robustness that was previously hand-coded now comes naturally and automatically with Seam.
In the source code of the example application you can see how declarative state management and integrated validation are used to achieve robustness.

3.4.2. Overview of the booking example

The project structure is identical to that of the previous project. To install and deploy this application, refer to Section 3.1, “Using the Seam examples”. Once you have successfully started the application, you can access the application by pointing the browser to http://localhost:8080/seam-booking/
The application uses six session beans to implement the business logic for the following features:
  • AuthenticatorAction provides the log in authentication logic.
  • BookingListAction retrieves existing bookings for the currently logged in user.
  • ChangePasswordAction updates the password of the currently logged in user.
  • HotelBookingAction implements booking and confirmation functionality. This functionality is implemented as a conversation, so HotelBookingAction class is an important class in the application.
  • HotelSearchingAction implements the hotel search functionality.
  • RegisterAction registers a new system user.
Three entity beans implement the application's persistent domain model:
  • Hotel is an entity bean that represents a hotel.
  • Booking is an entity bean that represents an existing booking.
  • User is an entity bean that represents a user who can make hotel bookings.

3.4.3. Understanding Seam conversations

This tutorial concentrates on 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 the application perspective, it is important that searching remains separate. This will help users to 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 that cannot be transferred is either added to the HttpSession or recorded to the database at the beginning and end of each request.
As the database is the least-scalable tier, it drastically reduces scalability. The extra traffic to and from the database also increases latency. In order to reduce redundant traffic, Java applications introduce a data cache to store commonly-accessed data between requests. However, invalidation is based upon an LRU policy, rather than whether the user has finished using the data. Therefore, this data cache is inefficient. This data cache is shared between concurrent transactions, which introduces issues associated with keeping the cached state consistent with that of the database.
State held in the HttpSession suffers similar issues. The HttpSession can be used to store true session data; data common to all requests between user and application. However, for data related to individual request series, HttpSession is not effective. Conversations stored in HttpSession break down quickly when dealing with multiple windows or the back button. Without careful programming, data in HttpSession can grow large, and make 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 improves conditions by introducing conversation context as a first class construct. Conversation state is stored safely in the conversation context, with a well-defined life cycle. Also, 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 beans are sometimes regarded as detrimental to scalability. 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. If used correctly, stateful session beans pose no scalability problems. However, if you are uncomfortable or unfamiliar with the use of stateful session beans, Seam also allows the use of POJOs.
The booking example shows one way in which stateful components with different scopes can collaborate to achieve complex behaviors. The main page of the booking application allows you to search for hotels. Search results are stored in the Seam session scope. When you 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 3.16. 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 HotelSearchingAction.java 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 hotels conversation variable.

4

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

Example 3.17. main.xhtml

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

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

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

1

The RichFaces Ajax <a:ajax> tag allows a JSF action event listener to be called by asynchronous XMLHttpRequest when a JavaScript event like keyup occurs. Also, the render attribute allows you to 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 allows you to display an animated image while you wait for asynchronous requests to return.

3

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

4

The Seam <s:link> tag allows you to attach a JSF action listener to an ordinary (non-JavaScript) HTML link. The advantage of this over the standard JSF <s:link> is that it preserves the operation of "open in new window" and "open in new tab". Also notice the use of 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 allows you to use parameters on any method binding expression.
All the navigation rules are in WEB-INF/pages.xml file, and discussed in Section 8.7, “Navigation”.
The main page of the application displays search results dynamically as you type and pass a selected hotel to the selectHotel() method of HotelBookingAction.
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 3.18. 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

A conversation-scoped stateful session bean uses an EJB3 extended persistence context, to manage entity instances for the entire 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 hotel context variable is 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 is not destroyed at the end of the request. Instead, the current conversation context is re-associated 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 is destroyed at the end of the request.

5

This EJB remove method is 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 the state related to this work in its instance variables. This code is much cleaner and simpler than getting and setting HttpSession attributes.
Also, you 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 can work on creating two different hotel reservations at the same time. If you leave one conversation inactive for a long time, 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.

3.4.4. The Seam Debug Page

The WAR also includes seam-debug.jar. To make the Seam debug page available, deploy seam-debug.jar in WEB-INF/lib alongside Facelets, and set the debug property of the init component as shown here:
<core:init jndi-pattern="${jndiPattern}" debug="true"/>
The debug page allows you to browse and inspect the Seam components in Seam contexts associated with your current log in session. Just point your browser at http://localhost:8080/seam-booking/debug.seam .