Rate this page del.icio.us  Digg slashdot StumbleUpon

Continuing the Conversation — Understanding Seam Nested Conversations

by Jacob Orshalick

What is a Long Running Conversation?

The concept of conversations has been popularized recently by a rush of frameworks providing more fine-grained control over state management. The age old issue of maintaining state throughout web interactions with a user has been a constant difficulty for developers. While the HTTP Session provides a manner of maintaining state between requests for a specific user, it is shared throughout the user’s interaction with the application. This can lead to hard to debug situations as data is shared between potentially unrelated sections of the application and is a constant source of memory leaks. Conversations offer an alternative to this approach that allows state to be scoped to a unit of work and automatically handles memory cleanup when a conversation is no longer in use.

Seam provides a conversation model that makes it simple for a developer to create applications with robust state management. Through simple annotation a developer can begin and end state management. Many conversations can run concurrently without any concerns for state integrity and the back button just works. Seam ships with the seam-booking example, which allows a user to book hotels. State management can be difficult when developing this type of application, but the Seam conversation model makes it simple. To follow through the example, access http://seam.demo.jboss.com and login as user: demo, password: demo. Once logged in, if you select Find Hotels, you see the results shown in Figure 1.

Figure 1: The results of a basic Find Hotels search in the Seam booking example. (Click on image to open larger version in a new window.)

Let’s suppose a user selects View Hotel for one of the hotels and then decides to book this hotel. The result of this action is shown in Figure 2.

Figure 2: The result of selecting a hotel for booking. (Click on image to open larger version in a new window.)

In the middle of booking the hotel, the user opens a new window and selects a different hotel for booking. Again the user proceeds to the booking section. What happens if the user returns to the original window and selects Proceed? If the current hotel being booked is maintained in a session variable and there are no fancy tricks being used, the user may end up booking the wrong hotel, or at least wrong from the user’s perspective. As you can imagine, similar situations can occur when using the back-button.

Generally, these situations are avoided through some clever tricks (e.g. capture the back button and re-post the form to get updated data, do not allow submit to occur, etc). These clever tricks work around what the user really wants to do and enforce what we as developers want them to do. Seam’s nested conversation model allows us to achieve what the user wants.

So why does this work in the booking demo? What fancy session tricks are being used to accomplish multi-window operation? This is actually quite simple when using Seam. Each booking is a conversation with the user or, as it is termed in Seam, a long-running conversation. A long-running conversation is a conversation that lasts more than one request. A long-running conversation has a beginning and an end. The following listing demonstrates how simple it is to begin a long-running conversation.

@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking
{
...

   @Begin
   public void selectHotel(Hotel selectedHotel)
   {
       hotel = em.merge(selectedHotel);
   }

...
        

Thus, when the user selects a hotel, we begin a long-running conversation that maintains state between requests. The state of each booking is completely unique as each long-running conversation started from selecting a hotel is completely independent. The user can have many bookings occurring in parallel without any concerns as shown in Figure 3.

Figure 3: A group of concurrent conversations within the same user session each uniquely identifiable by id. (Click on image to open larger version in a new window.)

Seam injects the appropriate hotel and booking instance based on the conversation context. Multi-window operation is resolved and the back button just seems to work. In addition, you may notice that the user instance is shared across conversation contexts. The user instance is scoped to the session, as shown in the following listing, making it available to each conversation context.

@Entity
@Name("user")
@Scope(SESSION)
@Table(name="Customer")
public class User implements Serializable
{
   ...
        

You may be wondering why Seam uses the term long-running conversation rather than simply conversations. Actually, Seem initiates a temporary conversation on each request and unless that conversation is promoted to long-running, it is ended once the request is complete. Thus, @Begin simply promotes a temporary conversation to long-running, which indicates to Seam that the conversation should be maintained across requests. Similarly, @End indicates that a previously long-running conversation should be ended once the request is complete.

Long-running Conversations Are Not Enough

So this seems great you say, why would I need anything else? Long-running conversations provide a great mechanism for maintaining consistency of state in an application. Unfortunately, simply beginning and ending a long-running conversation is not always enough. In certain situations, multi-window operation and usage of the back button can still result in inconsistencies between what the user observes and the reality of the application’s state.

For example, in the Seam booking application, let us add a new requirement. A user can not only book hotels, but when booking a hotel certain rooms may be available depending on the booking dates selected. In addition, the hotels would like us to provide in-depth descriptions of the rooms to entice users to upgrade their room preference. This would require a more wizard-style interface to achieve this behavior. The user selects the hotel for booking as shown in Figure 4.

Figure 4: Refactored booking screen that only requires entry of check-in date and check-out date. (Click on image to open larger version in a new window.)

The Select Room button then sends the user to a list of available rooms as shown in Figure 5.

Figure 5: Additional room selection screen that allows user to select a room for booking. (Click on image to open larger version in a new window.)

The user can select any available room and it will now appear as part of the booking package. Next let’s suppose the user opens another window with the room selection screen. In that screen the user selects the Wonderful Room and proceeds to the confirmation screen. In the other window the user decides to see what it would be like to live the high-life, selects the Fantastic Suite for booking, and again proceeds to confirmation. After reviewing the total cost, the user remembers that his or her credit card is near its limit! The user returns to the window showing Wonderful Room and selects confirm. Sound familiar?

Though we are within a long-running conversation, we are not protected from multi-window operation within that conversation. Similar to issues with the HTTP session, if a conversation variable changes, it affects all windows operating within the same conversation context. This results in the user being charged for a room upgrade that was not intended and a nasty phone call to customer service follows.

Continuing the Conversation

Seam’s conversation model provides a simplified approach to continuations. If you are familiar with the concept of a continuation server you are aware of the capabilities they provide including seamless back-buttoning and automatic state management. A user session has many continuations that are simply snapshots of state during execution and the continuations can be reverted to at any time. If you are not familiar with this concept, not to worry, Seam makes it simple.

The simple conversation model we discussed before is only part of the equation. In Seam’s conversation model you also have the ability to nest conversations. A conversation is simply a state container as we saw in Figure 3. Each booking occurs in its own conversation. Based on the conversation id (or cid for short), the appropriate hotel and booking is injected each time the HotelBookingAction is accessed.

Nesting a conversation provides a state container that is stacked on the state container of the original or outer conversation. Any objects that are set into the nested conversation’s state container do not affect the objects accessible in the parent conversation’s state container. This allows each nested conversation to maintain its own unique state as shown in Figure 6. When Seam performs a lookup of the roomSelection object, it looks at the current conversation determined by the conversation id (cid). Thus, the appropriate roomSelection is injected based on the user’s conversation context. In addition, the appropriate hotel and booking instances are also injected based on the outer conversation. Let us explore how this impacts our previous scenario.


Figure 6: A conversation with 2 nested conversations. Each nested conversation has its own unique state as well as access to the parent conversation state. (Click on image to open larger version in a new window.)

In the previous example, when the user reaches the Book Hotel screen, the application is operating within a long-running conversation. When the user selects the Select Room button, we again show the list of available rooms. Once a room is selected, a conversation is nested. Thus, regardless of whether the user opens multiple windows, the state is unique for each room selection.

Seam accomplishes this by storing the cid along with the view. The cid uniquely identifies the current conversation context. When the view is posted back to the server, Seam processes the request and retrieves the cid associated with that view. Seam then processes the action within the context of the retrieved conversation. Thus, the appropriate nested conversation is restored unaffected by a roomSelection made in any other nested conversation.

So how difficult is it to achieve this magic? The following listing contains the RoomPreferenceAction that allows the user to select a Room on the rooms.seam page.

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

    @Logger private Log log;

    @In(required=false)
    @Out
    private Hotel hotel;

    @In(required=false)
    @Out(required=false)
    private Booking booking;

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

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

    @Factory("availableRooms")
    public void loadAvailableRooms()
    {
      this.availableRooms =
            this.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)
    public String selectPreference()
    {
      // seam takes care of everything for us here.  we don't have
      // to do anything other than send the appropriate outcome to
      // forward to the payment screen.
      log.info("Room selected");

      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)
    public String cancel()
    {
      log.info("ending conversation");

      return "cancel";
    }

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

As you can see, when the user selects a room, we nest a conversation. Through use of Seam’s @DataModel and @DataModelSelection we are able to simply outject the room selected back to the nested conversation context once the room is selected. The user is then sent to the payment screen through a navigation rule defined in pages.xml. It’s that simple. When rendering the payment screen the new roomSelection instance is retrieved from the nested conversation context. Similarly, when the confirmation screen is displayed, the roomSelection is retrieved from the context as shown in Figure 7.


Figure 7: The room selection being displayed on the payment.xhtml view-id. (Click on image to open larger version in a new window.)

If the user confirms a booking, the correct roomSelection is always found for the current conversation regardless of multi-window operation.

You may then ask, what happens once the user has confirmed the booking for the hotel and room? What if the user then goes back to the Wonderful Room and confirms? We end the entire conversation stack when the user confirms a booking. Seam recognizes that the conversation has ended and redirects the user to the no-conversation-view-id shown in the following listing. Ending the conversation stack is described further in the next section.

<pages no-conversation-view-id="/main.xhtml"
       login-view-id="/home.xhtml"\>
  ...
        

Note here we also specify a global definition of no-conversation-view-id that is applied to each page definition. Should a page entry be specified as conversation-required=true or if a conversation is no longer valid, the user is redirected to the provided view-id.

Nesting a conversation

When a nested conversation is started, the semantics are the same as beginning a normal long-running conversation except that a nested conversation is pushed onto the conversation stack. As mentioned previously, the state of the nested conversation hs 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, other nested conversations can exist concurrently stacked on the same outer conversation, allowing independent state for each.

This can be accomplished in several ways. Here are some examples:

  • Annotation-based – begin a nested conversation if the method return-type is void or the method does not return null.
    @Begin(nested=true)
    public void timeForAction() {
    	System.out.println(“I must take action… print something!”);
    }
    
                            
  • View based – begin a nested conversation when a link is selected.
    <s:link value=”Nest Conversation”
                action=”myAction.willBeNested”
                propagation=”nest” />
                            
  • In pages.xml – begin a nested conversation when a view-id is accessed.
    <page view-id=”nestedView.xhtml”>
    	<begin-conversation nested="true" />
    
    </page>
                         

Once begun, the nested conversation is pushed onto the ConversationStack. In our extended seam-booking example, the stack consists of the original outer long-running conversation and each of the nested conversations, as we saw in Figure 6.

If a @End is encountered, the ConversationStack is popped, meaning that the outer conversation is resumed effectively reverting the state back to the outer conversation’s state container. This is useful if the user is to cancel an action. For example, if the user is on the payment.xhtml view and selects Revise Room, the action roomUpgrade.cancel is annotated with @End(beforeRedirect=true), as shown previously in the RoomPreferenceAction. This ends the nested conversation clearing all nested conversation state and effectively reverts back to the outer conversation. Specifying beforeRedirect=true ensures that when the next view is rendered, the outer conversation state is effective.

One important final note is ending the nested conversation when the user confirms the booking. Seam ends the entire ConversationStack when the root conversation is ended. The root conversation is the conversation that started it all. This can be accomplished programmatically through the code snippet found in the following listing.

private void endRootBeforeRedirect() {
	Conversation conversation = Conversation.instance();

	if(conversation.isNested()) {
	    conversation.root();
	}

	conversation.endBeforeRedirect();
}
        

When the user submits the booking, the endRootBeforeRedirect() method is invoked to end the entire ConversationStack.

Extending the seam-booking example with JBoss Developer Studio

The following tutorial guides you through how to add a nested conversation using the Seam wizards provided by JBoss Developer Studio (JBDS) or its open source cousin JBoss Tools. The tutorial demonstrates how easy JBDS makes it to implement the nested conversation demonstrated by the extended-booking example. To follow along, first import the three Eclipse projects in the extended-booking.zip example into your JBDS workspace. You need to modify the project properties to point them to an existing JBoss AS runtime and an existing Seam 1.2 runtime already installed on your system. Refer to JBDS or JBoss Tools documentation on how to do this. Then, follow the instructions below to create the RoomPreferenceAction as well as the Room entity.

Let’s begin by creating a new conversation for the room preference. Once the project is imported, select File -> New -> Seam Conversation.

Generally, Seam components are denoted by specifying the @Name annotation at the top of the class. Specifying the Seam component name sets the @Name annotation on your component. Note how as you specify the Seam component name that JBDS automatically completes the additional fields. This can help speed up development as well as aid in consistency.

A local interface as well as a stateful session bean implementing that interface is automatically generated with the names provided. There is no need to specify a method name here for the example. The page name automatically generates a page in the view directory. In order to follow the example, name your page rooms.

Once you have created the new RoomPreferenceAction stateful session bean and the rooms.xhtml page, use these templates to extend the seam-booking example to include the room selection screen.

Access the Local interface RoomPreference, and refactor the begin() method that was auto-generated to selectPreference(). Next access the RoomPreferenceAction bean and change @Begin to @Begin(nested=true). This nests a conversation when selectPreference () is executed. The following navigation rule should also be added to your pages.xml to redirect the user to our new view and refactor the setBookingDates method to return the outcome “rooms”.

<page view-id="/book.xhtml" conversation-required="true">

    <description>Book hotel: #{hotel.name}</description>

    <navigation from-action="#{hotelBooking.setBookingDates}">
        <rule if-outcome="rooms">
        	<redirect view-id="/rooms.xhtml"/>
        </rule>

    </navigation>
</page>
        

Now we can create the Room entity using the Seam wizard. In the same menu, select File -> New -> Seam Entity.

Entities are specified in JPA by placing the @Entity annotation at the top of the class as well as an id that is generally an attribute annotated with @Id. The wizard generates a class with the provided Entity class name and includes the appropriate annotations as well as @Version and name attributes. In addition, the wizard generates an EntityHome and EntityQuery instance to enable simple data-access by implementing the provided method stubs. There is also a roomList.xhtml and room.xhtml page that can be completed to allow administration of Hotel rooms. Implementation of these additional features has not been included in the provided example and is left as an exercise for the reader.


Once the Room entity has been generated, the remainder of the implementation can be completed by referring to the extended-booking example provided. This is left as an exercise for the reader.

Deploying the example application in JBDS

In order to deploy the example, start the JBoss application server instance you selected for the project, as shown below.


To deploy the application, right-click the extended-booking-ear project and select Run As -> Run on Server.


This deploys the application to the JBDS JBoss application server instance as an exploded ear allowing for automated hot deployment.

Additional Considerations

Currently an open issue and ongoing discussion with respect to the existing nested conversation implementation is the topic of master-details editing of managed entities. A Seam-managed persistence context (SMPC) enables managed entities to remain managed throughout a long-running conversation when manual flushing is enabled. This provides seamless persistence scoped to the long-running conversation and avoids the common LazyInitializationException when lazy-loading is enabled.

In the current implementation, an SMPC is shared between an inner and outer conversation. This results in any changes made in the nested conversation to be flushed by the SMPC in the outer conversation and vice versa should a flush be initiated. Thus, if the state of a managed entity is altered within the course of a nested conversation these changes are persisted even if the user returns to the outer conversation. A potential alternative is to initialize a new SMPC instance within the nested conversation and merge entities that may be changed with the new SMPC instance, but this approach is not directly supported. Work in this area could prove beneficial to furthering Seam’s continuation support.

In Conclusion

Seam offers a very attractive approach to state management through its simple conversation model. Complicated issues developers struggled with in the past including multi-window operation and back-buttoning are handled seamlessly. While nested conversations could still benefit from additional continuation support including snapshots of managed entities, nested conversations are a powerful feature that allows applications to behave according to a user’s expectations rather than the developer’s.

Resources

About the author

Jacob Orshalick is a consultant living in Dallas, Tx who is actively involved in the JBoss Seam community. He has seven years of software development experience and has spent much of that time developing or extending web frameworks for his clients. He will also be co-author of the upcoming second edition of JBoss Seam: Simplicity and Power Beyond Java EE .


This article was edited 2007-12-09 04:30 UTC to reflect final product naming for JBoss Developer Studio.

Comments are closed.