JBoss Seam and conversation boundaries

In the web site we are building we have a set of search pages to identify records, and a long list of CRUD pages for various aspects of those records. This section of the application looks and feels just as a standard web application, where as a user you are engaged in only one activity at a time and none of the business transactions you engage in span more than a single database transaction (unlike, say, a wizard).

Another section of the application works completely differently – it is a document editor that can edit complex structured documents associated with records in the system. This cries out for the ability to have multiple editors available, which is exactly the kind of thing that can be modeled by a Conversation in Seam .

Supporting the document editor feature we have several command buttons distributed throughout the application that allow you to add, edit, or view (in PDF form) these documents.  Once in a document editor, you can leave the editor, visiting the rest of the application, and resume it with a click of a button.

Implementing this turned out to be a little trickier than expected and required a solid understanding of conversation boundaries, or when a conversation comes into existence, and when it leaves.

Here are the key players in the architecture:

SeamEditorCD1

So the navigation flow is very simple – the user visits a search results page, and from there opens an editor. In between the visit to the search-results page and the editor being ready to use in the browser,  the various other components shown are accessed. I’ve explicitly omitted conversation boundaries from this diagram since there are so many ways to start and stop the conversation and it’s the core of what I want to talk about.

In the diagram, the component classes are omitted since they are irrelevant in Seam.

  • editorCollection: Keep track of conversations associated with specific documents.
  • search-results.xhtml: Display a list data items, each with action buttons.
  • editorActions: Contains the code backing the buttons in search-results.xhtml, acts on documents before they are being edited.
  • document: An entity instance of a document being edited or viewed
  • editorManager: Contains the code backing the buttons in editor.xhtml, allowing operations to be performed on a document while it is being edited.
  • editor.xhtml: The actual editor page – consists of a range of buttons to act on the document or to leave the editor, along with document specific widgets that allow data to be entered into the document.
  • pages.xml: This is the global Seam page configuration file, similar to faces-config.xml in JSF.

Navigation

pages.xml is the central piece that establishes navigation among pages in terms of responses to actions triggered by buttons on the page. A simple example might simply be:

[xml]<pages>
<page view-id="search-results.xhtml">
<navigation from-action="#{editorActions.edit(documentId)}">
<redirect view-id="editor.xhtml" />
</navigation>
</page>
</pages>[/xml]

This action would correspond to a link from search-results.xhtml:

[xhtml]<s:link action="#{editorActions.edit(documentId)}" />[/xhtml]

The from-action attribute in pages.xml is not executed – it simply identifies the operation that the navigation rule applies to by being the same exact string. When the link is clicked, the edit() function is executed, and the editor.xhtml view is loaded.

Starting a conversation

This navigation rule and the code listed so far does not actually start a conversation, which means that anything the edit() function does is going to do will act on the session as a whole (since it is a Seam SESSION component). So we have no segregation between document editor instances.

Ignoring the ability to declare that a conversation is required (that’s really just defensive coding, and who wants to see exception handling code?); there are three main places you can instruct Seam to start up a conversation:

* Java 1.5 Annotations provided by Seam (@Begin) on the components
* Attributes on s:link and s:button or tags applied to JSF components; all provided by the Seam UI tag library
* Within page rules in pages.xml using directives like

There are good reasons for using each, but the necessarily page and component spanning nature of conversations make the third option a good choice. The declarative features of pages.xml allow the start and end points of conversations to be placed in a single file, centralizing the lifetime of a conversation, while still allowing the option of implementing conditional navigation logic with EL expressions.

Document editor pages.xml with conversations

[xml]<pages>
<page view-id="search-results.xhtml">
<navigation from-action="#{editorActions.edit(documentId)}">
<start-conversation />
<redirect view-id="editor.xhtml" />
</navigation>
</page>
<page view-id="editor.xhtml">
<navigation from-action="#{editorManager.discard()}">
<end-conversation />
<redirect view-id="search-results.xhtml" />
</navigation>
</page>
</pages>[/xml]

This simple fragment easily shows the ability to cycle between the two pages, starting a conversation before being in the editor and ending a conversation when leaving.

Exactly when does a conversation come to exist?

Upon a click from the search results page, an action method on the editorActions component is invoked. Since the editorAction methods are working on any of the iterated search results, we need to pass the selected record to the action method.

This data is the documentId of the document being edited, (or equivalently the documentTypeId of the document being added). Using this parameter we retrieve from persistence all the data required to display and format the document. Obviously all this information needs to be scoped to the current conversation context in order for it to be segregated from other instances of editors.

Which poses the question: is the conversation context active when the editorActions.edit(documentId) method is executing? If the answer is yes, we should be able to place variables into the conversation context from within editorActions.edit(documentId).

Unfortunately for our system the answer is no, which means this code is bad:

[java]import org.jboss.seam.annotations.*;
import static org.jboss.seam.ScopeType.*;
@Name("editorActions") public class EditorActions {
@In(required = false)
Conversation conversation;
public void edit(int documentId)
{
assert conversation != null;
// record the conversation editor instance
}
}[/java]

We find that during this method, access to the conversation object is unavailable. The obvious inference is that there is no active Seam conversation at this time.

Now it is possible that the tag in pages.xml acts differently to placing an @Begin annotation on an action method. However, the following code acts no differently:

[java]@Begin public void edit(int documentId)
{
assert conversation != null;
// record the conversation editor instance
}[/java]

The problem, and neglected alternate approaches

Event with a conversation context in place, the code performs a significant amount of startup of other components that rely on Conversation scoped data, but because Seam manages outjection through interceptors around method calls, any @Out annotated variables will not actually be placed into the conversation scope until the method returns. So all those other components cannot perform the work they need to do in response to code in the editorActions.edit(documentId) action method.

The issue could be resolved by starting the conversation on one page, and redirecting the user to a temporary page in which a document is not yet loaded, but in which the Seam conversation is fully active. We have tried this approach before, but routing the user indirectly does not give the application the slick feel we are looking for.

One thing we tried when creating a workaround that would allow us to place the loaded document into the conversation context was to observe the built-in seam event that is fired when a new conversation is created. This turned out to be a complicated path to take because the component dealing with creating the document is in the SESSION scope, meaning that it synchronized by Seam to ensure that there are no concurrent accesses. This prevented our observer from interacting with the session scoped editorActions bean as the action method was already executing.

Establishing and populating a Seam conversation context in one go

What we finally tried and have discovered to work well is to access directly the Conversation context from within the editorAction.edit(documentId) method:

[java]public void edit(int documentId)
{
Document d = loadDocument(documentId);
Context conversationContext = Contexts.getConversationContext();
conversationContext.set("documentData", d);
d.activate();
}[/java]

The d.activate() function can access conversation scoped variables manually placed in the conversation context by this method, despite the conversation not having been fully created, and despite any outjections (@Out annotated variables) not having yet been processed.

This is inline with Seam’s published behaviour in which there is always a conversation context present, but that it is temporary unless it has been converted to a long running conversation.

Final Solution

In the end we opted to leverage the Seam @Begin annotation on the editorActions methods that trigger a switch to an editor.  In this manner we ensure the creation of a Conversation is co-located with the functionality that is so tightly coupled to it. We did not do the same for the end of the conversation, since there are various buttons and links on the editor that assist with leaving the page, not all of which have action methods associated with them.  In addition, we decided to sink exceptions and internally log and email them, and altered the action methods to return an outcome string. This allows us to conditionally return to the results page if an error occurs creating an editor and also be able to display an appropriate message. This works nicely with Richfaces/A4J .

The pages.xml now looks like this:

[xml]<pages>
<page view-id="search-results.xhtml">
<navigation from-action="#{editorActions.edit(documentId)}">
<rule if-outcome="success">
<redirect view-id="editor.xhtml" />
<rule>
<rule if-outcome="bad-template">
<message severity="ERROR">Failed to create the editor because template #{errorTemplate} is broken</message>
</rule>
</navigation>
</page>
<page view-id="editor.xhtml">
<navigation from-action="#{editorManager.discard()}">
<end-conversation />
<redirect view-id="search-results.xhtml" />
</navigation>
</page>
</pages>[/xml]

As was mentioned above, we also opted to create a separate editorCollection bean. This bean was necessary purely to avoid concurrent access to a single Seam component. When a conversation is created, this bean manages the process of figuring out a human readable name for the conversation for use in conversation switcher components (i.e., to switch editors).

More on concurrent access to conversation components

As I’ve noted, a SESSION scoped bean cannot be accessed concurrently. Really, no Seam component can be treated as re-entrant. Synchronization on SESSION components is provided to you by Seam because it is likely that a user may double click a link or otherwise trigger multiple concurrent requests. Seam components in lower scopes like PAGE and EVENT do not need synchronization since there will be multiple instances of them created to respond to each activity.

CONVERSATION scoped components are synchronized by Seam by locking the whole conversation while the request is ongoing. New requests for the same conversation are blocked for up to 500ms (configurable), and if the conversation is still not free, then Seam returns a message. In my experience, those web requests received a blank document.

This is critical to understand if you need to make back-end round-trip requests to the web server – you cannot use the same conversation for those. But that’s a topic for another time.

One thought on “JBoss Seam and conversation boundaries

  1. Pingback: News JBoss Seam and conversation boundaries | Web 2.0 Designer

Comments are closed.