1.7.2. Understanding Nested Conversations

download PDF
The following code shows the behavior of the hotel booking application with entended behavior for nested conversations. Again, think of the code as a set of steps to be read in sequence.

Example 1.31. RoomPreferenceAction.java

@Stateful
@Name("roomPreference")
@Restrict("#{identity.loggedIn}")
public class RoomPreferenceAction implements RoomPreference 
{

    @Logger 
        private Log log;

    @In private Hotel hotel;                                                                  1
   
    @In private Booking booking;

    @DataModel(value="availableRooms")
    private List<Room> availableRooms;

    @DataModelSelection(value="availableRooms")
    private Room roomSelection;
    
    @In(required=false, value="roomSelection")
    @Out(required=false, value="roomSelection")
    private Room room;

    @Factory("availableRooms")
    public void loadAvailableRooms()
    {
        availableRooms = hotel.getAvailableRooms(booking.getCheckinDate(), 
                                                 booking.getCheckoutDate());
        log.info("Retrieved #0 available rooms", availableRooms.size());
    }

    public BigDecimal getExpectedPrice()
    {
        log.info("Retrieving price for room #0", roomSelection.getName());
      
        return booking.getTotal(roomSelection);
    }

    @Begin(nested=true)                                                                       2
    public String selectPreference()
    {
        log.info("Room selected");
      
        this.room = this.roomSelection;                                                       3
      
        return "payment";
    }

    public String requestConfirmation()
    {
        // all validations are performed through the s:validateAll, so checks are
        // already performed
        log.info("Request confirmation from user");
      
        return "confirm";
    }

    @End(beforeRedirect=true)                                                                 4
    public String cancel()
    {
        log.info("ending conversation");

        return "cancel";
    }

    @Destroy @Remove                                                                      
        public void destroy() {}    
}

1

The hotel instance is injected from the conversation context. The hotel is loaded through an extended persistence context so that the entity remains managed throughout the conversation. This allows us to lazily load the availableRooms through an @Factory method by simply walking the association.

2

When @Begin(nested=true) is encountered, a nested conversation is pushed onto the conversation stack. When executing within a nested conversation, components still have access to all outer conversation state, but setting any values in the nested conversation’s state container does not affect the outer conversation. In addition, nested conversations can exist concurrently stacked on the same outer conversation, allowing independent state for each.

3

The roomSelection is outjected to the conversation based on the @DataModelSelection. Note that because the nested conversation has an independent context, the roomSelection is only set into the new nested conversation. Should the user select a different preference in another window or tab a new nested conversation would be started.

4

The @End annotation pops the conversation stack and resumes the outer conversation. The roomSelection is destroyed along with the conversation context.
When we begin a nested conversation, it is pushed onto the conversation stack. In the nestedbooking example, the conversation stack consists of the external long-running conversation (the booking) and each of the nested conversations (room selections).

Example 1.32. rooms.xhtml

<div class="section">
  <h1>Room Preference</h1>
</div>

<div class="section">
  <h:form id="room_selections_form">
    <div class="section">
      <h:outputText styleClass="output" 
         value="No rooms available for the dates selected: " 
         rendered="#{availableRooms != null and availableRooms.rowCount == 0}"/>
      <h:outputText styleClass="output" 
         value="Rooms available for the dates selected: " 
         rendered="#{availableRooms != null and availableRooms.rowCount > 0}"/>
      
      <h:outputText styleClass="output" value="#{booking.checkinDate}"/>
      <h:outputText styleClass="output" value="#{booking.checkoutDate}"/>
      
      <br/><br/>
      
      <h:dataTable value="#{availableRooms}" var="room"                             1
         rendered="#{availableRooms.rowCount > 0}">
        <h:column>
          <f:facet name="header">Name</f:facet>
          #{room.name}
        </h:column>
        <h:column>
          <f:facet name="header">Description</f:facet>
          #{room.description}
        </h:column>
        <h:column>
          <f:facet name="header">Per Night</f:facet>
          <h:outputText value="#{room.price}">
            <f:convertNumber type="currency" currencySymbol="$"/>
          </h:outputText>
        </h:column>
        <h:column>
          <f:facet name="header">Action</f:facet>
          <h:commandLink id="selectRoomPreference" 
            action="#{roomPreference.selectPreference}">Select</h:commandLink>      2
        </h:column>
      </h:dataTable>   
    </div>
    <div class="entry">
      <div class="label">&#160;</div>
      <div class="input">
        <s:button id="cancel" value="Revise Dates" view="/book.xhtml"/>             3
      </div>
    </div>    
  </h:form>
</div>

1

When requested from EL, the #{availableRooms} are loaded by the @Factory method defined in RoomPreferenceAction. The @Factory method will only be executed once to load the values into the current context as a @DataModel instance.

2

Invoking the #{roomPreference.selectPreference} action results in the row being selected and set into the @DataModelSelection. This value is then outjected to the nested conversation context.

3

Revising the dates simply returns to the /book.xhtml. Note that we have not yet nested a conversation (no room preference has been selected), so the current conversation can simply be resumed. The <s:button> component simply propagates the current conversation when displaying the /book.xhtml view.
Now that you have seen how to nest a conversation, the following code shows how we can confirm the booking of a selected room by extending the behavior of the HotelBookingAction.

Example 1.33. HotelBookingAction.java

@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking
{
   
   @PersistenceContext(type=EXTENDED)
   private EntityManager em;
   
   @In 
   private User user;
   
   @In(required=false) @Out
   private Hotel hotel;
   
   @In(required=false) 
   @Out(required=false)
   private Booking booking;
   
   @In(required=false)
   private Room roomSelection;
   
   @In
   private FacesMessages facesMessages;
      
   @In
   private Events events;
   
   @Logger 
   private Log log;
   
   @Begin
   public void selectHotel(Hotel selectedHotel)
   {
      log.info("Selected hotel #0", selectedHotel.getName());
      hotel = em.merge(selectedHotel);
   }
   
   public String setBookingDates()
   {
      // the result will indicate whether or not to begin the nested conversation
      // as well as the navigation.  if a null result is returned, the nested
      // conversation will not begin, and the user will be returned to the current
      // page to fix validation issues
      String result = null;

      Calendar calendar = Calendar.getInstance();
      calendar.add(Calendar.DAY_OF_MONTH, -1);

      // validate what we have received from the user so far
      if ( booking.getCheckinDate().before( calendar.getTime() ) )
      {
         facesMessages.addToControl("checkinDate", 
                                    "Check in date must be a future date");
      }
      else if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) )
      {
         facesMessages.addToControl("checkoutDate", 
                                    "Check out date must be later than check in date");
      }
      else
      {
         result = "rooms";
      }

      return result;
   }
   
   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() );
   }
   
   @End(root=true)                                                                          1
   public void confirm()
   {
      // on confirmation we set the room preference in the booking.  the room preference
      // will be injected based on the nested conversation we are in.
      booking.setRoomPreference(roomSelection);                                             2

      em.persist(booking);
      facesMessages.add("Thank you, #{user.name}, your confimation number" +
                        " for #{hotel.name} is #{booking.id}");
      log.info("New booking: #{booking.id} for #{user.username}");
      events.raiseTransactionSuccessEvent("bookingConfirmed");
   }
   
   @End(root=true, beforeRedirect=true)                                                     3
   public void cancel() {}
   
   @Destroy @Remove
   public void destroy() {}
}

1

Annotating an action with @End(root=true) ends the root conversation which effectively destroys the entire conversation stack. When any conversation is ended, its nested conversations are ended as well. As the root is the conversation that started it all, this is a simple way to destroy and release all state associated with a workspace once the booking is confirmed.

2

The roomSelection is only associated with the booking on user confirmation. While outjecting values to the nested conversation context will not impact the outer conversation, any objects injected from the outer conversation are injected by reference. This means that any changing to these objects will be reflected in the parent conversation as well as other concurrent nested conversations.

3

By simply annotating the cancellation action with @End(root=true, beforeRedirect=true) we can easily destroy and release all state associated with the workspace prior to redirecting the user back to the hotel selection view.
Feel free to deploy the application and test it yourself. Open many windows or tabs, and attempt combinations of various hotel and room preferences. Confirming a booking will always result in the correct hotel and room preference with the nested conversation model.
Red Hat logoGithubRedditYoutubeTwitter

Learn

Try, buy, & sell

Communities

About Red Hat Documentation

We help Red Hat users innovate and achieve their goals with our products and services with content they can trust.

Making open source more inclusive

Red Hat is committed to replacing problematic language in our code, documentation, and web properties. For more details, see the Red Hat Blog.

About Red Hat

We deliver hardened solutions that make it easier for enterprises to work across platforms and environments, from the core datacenter to the network edge.

© 2024 Red Hat, Inc.