Version 2

    Stateful conversations

     

    The hotel search form uses the bean, which is mapped from the HotelBookingAction session bean via the Seam @Name annotation.

     <h:inputText value="#{hotelBooking.searchString}" ></h:inputText>
     <h:commandButton value="Find Hotels" 
                     action="#{hotelBooking.find}" 
                     styleClass="button" ></h:commandButton>
    

    Now, let's look into the relevant parts of the HotelBookingAction class. First, we note that it is a stateful bean since its state (e.g., the hotels found) is used in multiple pages. Second, it uses the @LoggedIn annotation to indicate that the user must be logged in to use this bean.

     @Stateful
     @Name("hotelBooking")
     @Interceptor(SeamInterceptor.class)
     @Conversational(ifNotBegunOutcome="main")
     @LoggedIn
     public class HotelBookingAction implements HotelBooking, Serializable {
     
       @PersistenceContext(type=EXTENDED)
       private EntityManager em;
       
       private String searchString;
      
       @DataModel
       private List <Hotel> hotels;
       @DataModelSelectionIndex
       private int hotelIndex;
       
       @Out(required=false)
       private Hotel hotel;
       
       // ... ...
       
       @Begin
       public String find() {
          hotel = null;
          String searchPattern = 
            searchString==null ? "%" : '%' + 
            searchString.toLowerCase().replace('*', '%') + '%';
          
          hotels = em.createQuery(
            "from Hotel where lower(name) like :search " + 
            "or lower(city) like :search or lower(zip) like :search " + 
            "or lower(address) like :search")
                .setParameter("search", searchPattern)
                .setMaxResults(50)
                .getResultList();
          
          return "main";
       }
       
       public String getSearchString() {
          return searchString;
       }
     
       public void setSearchString(String searchString) {
          this.searchString = searchString;
       }
       
        // ... ...
     }
    

    What you see here is that the HotelBookingAction.find() method takes the searchString property collected from the form and find a list of hotels in the database. The EntityManager in this class must have EXTENDED scope since it needs to manage entity objects across multiple threads (i.e., multiple page requests to the bean methods). This is generally true for EntityManager objects in all stateful session beans. Now, let's look at how the conversation is constructed.

     

    The Seam @Conversational annotation declares this as a conversational component that cannot be invoked outside of a long-running conversation that was started by a call to its @Begin method. If such an invocation does occur, Seam returns the ifNotBegunOutcome to JSF. 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, which happens after you finished the booking.

     

    After the find() method finds the hotels from the database, it populates them into a List object hotels. The hotels property is marked with the @DataModel annotation, which tells SEAM to convert it to a JSF ListDataModel. This makes it easy to implement clickable lists for search screens. The @DataModelSelectionIndex annotation defines a field or get/set pair as holding the row index for the corresponding @DataModel property. Therefore, in the main.xhtml page, it is trivial to display the hotel search results with no JSF display data conversion needed.

     <div class="section">
         <h:outputText value="No Hotels Found" 
                       rendered="#{hotels != null and empty hotels}"></h:outputText>
         <h:dataTable value="#{hotels}" var="hot" 
                       rendered="#{not empty hotels}">
              <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}
              </h:column>
              <h:column>
                   <f:facet name="header">Zip</f:facet>
                   #{hot.zip}
              </h:column>
              <h:column>
                   <f:facet name="header">Action</f:facet>
                   <h:commandLink action="#{hotelBooking.selectHotel}">View Hotel</h:commandLink>
              </h:column>
         </h:dataTable>
     </div>
    

    The "View Hotel" action method, HotelBookingAction.selectHotel(), does not have any annotation. It is just a method inside the conversation started by find(). It selects the hotel and pushes JSF to the next page.

     @Stateful
     @Name("hotelBooking")
     @Interceptor(SeamInterceptor.class)
     @Conversational(ifNotBegunOutcome="main")
     @LoggedIn
     public class HotelBookingAction implements HotelBooking, Serializable {
    
       public String selectHotel() {
          if ( hotels==null ) return "main";
          setHotel();
          return "selected";
       }
       
       private void setHotel() {
          hotel = hotels.get(hotelIndex);
       }
       
       // ... ...
     }