Vita Rara: A Life Uncommon

Using a Conversation Scope in Struts 2


Categories: | | |

What is a conversation scope?

A conversation scope fits between a Session scope and a Request scope in J2EE terms. An object in a session is specific to a particular user of a web app. If you place an object in the users session it will be available on every page hit the user makes.

A conversation scope defines a long lived set of data that is specific to a process, such as configuring a product, or filling out a customers personal and credit information. It differs from a session scope in that one user should be able to have multiple of th same type of conversation at the same time. You might actually have to help more than one customer at a time, or configure multiple products, one on each tab in your web browser.

Does Struts 2 Support This?

Not directly. I have written an Interceptor that implements a Conversation Scope for possible inclusion in Struts 2 in the future. We'll see what happens. In the mean time I've created an example, including the supporting classes.

How Does it Work?

This extension to Struts 2 consists of two classes, an Interceptor and an Interface for your Actions to implement. That's it.

The Interceptor does most of the heavy lifting. It creates new conversation id's. Marshals the data from the users conversation scope into your Action, and cleans up when you mark the conversation complete.

The interface consists of only a few methods:

package org.apache.struts2.interceptor;

public interface ConversationScopeAware {

    public static final String S2_CONVERSATION_SCOPE = "S2_CONVERSATION_SCOPE";
    public static final String S2_CONVERSATION_ID = "S2_CONVERSATION_ID";
    public static final String S2_VALUE_STACK_MODEL_KEY = "s2cmodel";
    public static final String S2_VALUE_STACK_MODEL_ID_KEY = "modelid";

    /** Get the id of the current conversation the action is handling. */
    public String getConversationId ();

    /** Set the id of the current conversation the action is handling. */
    public void setConversationId (String in);

    /** Get the conversation model. */
    public Object getConversationModel ();

    /** Set the conversation model. */
    public void setConversationModel (Object in);

    /** 
     * Prepare the conversationModel. So it can be saved in the conversationScope and
     * pushed onto the top of the value stack. The action needs to maintain a reference
     * to the prepared model.
     */
     public Object prepareConversationModel ();

     /** 
      * Find out if the conversation is finished. If true the conversation model can 
      * be removed from the conversationScope. 
      */
     public boolean isConversationFinished ();
}

The most important concept to grasp is the conversationModel. This is very similar to the ModelDriven actions. Basically you have some Object that represents the data you want to collect and process as you have a conversation with your user. Your model object can be almost anything, including your rich domain objects. I would discourage you from using a Map though.

When Struts 2 receives a request for a ConversationScopeAware action it will check to see if there is a stored conversation using the conversation id. If there is no conversation it will ask the Action to prepare the conversation model by calling ConversationScopeAware#prepareConversationModel(). This is your chance to setup your data model that you will fill in over your conversation. Once you have prepared your model the interceptor will push it onto the top of the stack, so the properties can be filled in with the user's parameters.

In your view you will need to include the conversation id in a hidden field of your form:

<s:hidden name="S2_CONVERSATION_ID" value="%{conversationId}" />

Once you have finished with the conversation you can mark it for removal from the users conversation scope. After running the action the interceptor will call isConversationFinished(). If this returns true it will dispose of the saved conversation model.

An Example

Attached to this post is a Maven project, which demonstrates the use of a conversation. Download it and build it using: mvn package. This will create a war file under the target directory. Deploy that to your favorite servlet container and point your browser at it (ex: http://localhost:8080/tutorial/).

You will be prompted to enter your name, and click the next button. Also at the bottom there is a link to start another conversation. You can have as many conversations going as you like. The data for each of them is independent.

Take a look at src/main/java/tutorial/ConversationAction.java to see how it all works. In short you have a simple Action, which includes an inner class to store its data called ConversationModel. It instantiates a copy of ConversationModel when prepareConversationModel() is called. The Interceptor stores it transparently for the Action. On subsequent hits the model will already be injected into the action by the Interceptor allowing the Action to act on any data that has been previously entered by the user.

The example takes the user through a few steps using logic in the execute() method, eventually sending the result page which displays all of the collected data.

What's Missing?

This example doesn't show use of the prepare() method, nor the validate() method in conjunction with our conversation scope. You can use the prepare() to add data to your conversation model as the user fills out forms, and you can use th validate() to validate each screen the user submits.

AttachmentSize
tutorial.tar_.gz15.45 KB

Conversation Scope doesn't work after a redirect

Hi,

I want to implement your conversation interceptor, but have a little problem while using a redirect-action as result type (want to implement the POST-REDIRECT-GET Pattern):

I downloaded your tutorial and changed the struts.xml like this:

struts.xml

[action name="conversation" class="tutorial.ConversationAction"]
[interceptor-ref name="params" /]
[interceptor-ref name="conversation-scope" /]
[interceptor-ref name="defaultStack" /]
[result name="input" type="redirect-action"]/conversationRedirect[/result]
[result name="success"]/ConversationSuccess.jsp[/result]
[/action]

[action name="conversationRedirect" class="tutorial.ConversationAction" method="input"]
[interceptor-ref name="params" /]
[interceptor-ref name="conversation-scope" /]
[interceptor-ref name="defaultStack" /]
[result name="input"]/ConversationInput.jsp[/result]
[/action]

So I did a redirect instead of showing the view. The input method of the conversationRedirect action just returns "input". My problem is, that the page count of the model is not increased. To at the result page the count is 0 instead of 1.

Maybe you can help me with this problem.

Thanks

Brian
duckmailer@googlemail.com

Difficult to use with input validators

This interceptor seemed like a good idea for the use case I'm implementing in my application - a two-step user registration process. What I'm finding is that, in practice, the ConversationInterceptor is difficult to use because it is interacting badly with Struts2 input validators. It forces you to structure your input validation configuration in one specific way.

Allow me to explain further.

I started off with two action classes, two separate JSPs, and two validation.xml configuration files. The ConversationScopeAware interface has an unstated requirement that all aspects of the conversation take place within one action class, as the lifecycle of the ConversationModel is determined by the prepareConversationModel() and isConversationFinished() methods in the ConversationScopeAware interface. (I assume if I use two separate action objects, each will get a separate ConversationModel object, sort of defeating the purpose of the whole thing).

So, I refactored to put the entire conversation into one action class. Because the Struts2 validation mechanism is hardwired to go to the "input" result on any validation error, I have to combine all the user forms into one JSP as well. Furthermore, I also have to put the input validation into one .xml file.

At this point, only a subset of the input fields are displayed to the user at any step. But the Struts validator validates them all at every step, flagging non-existent fields as not having valid input.

To work around this limitation, I can implement separate methods on the action object for each step in the conversation, but I'm then forced to configure separate validation configuration files for each method in my action (e.g. action-method-validation.xml).

So, the options for how I use and configure input validations are somewhat limited, unless you can see an alternative that I'm missing (which could be possible, as I am a Struts2 neophyte).

I'd like to see some support for extending conversations across action classes. That extra bit of flexibility would make the ConversationInterceptor play a bit nicer with the rest of Struts2.

> To work around this

> To work around this limitation, I can implement separate
> methods on the action object for each step in the
> conversation, but I'm then forced to configure separate
> validation configuration files for each method in my action
> (e.g. action-method-validation.xml).

In my opinion Struts2 doesn't support "action-method-validation.xml" as an validation declaration. Does it work for you?

method level validation

hi
thanks lot its working fine at my end.

action-method-validation.xml

yes it is working but still there are new problems associated with it. which i am trying to resolve otherwise i will give up this solution.

OutOfMemoryException

Just came across this rather interesting post, and have been integrating the interceptor into one of my applications. An interesting point I believe (although have not yet encountered) is that the interceptor uses an internal map for managing conversations, however if enough users start a conversation but never finish it (say across 4 actions for example with the first creating the conversation model and the last being the 4th action will return true via isConversationFinished where the other three return false) and the user moves away to another page after action 1 or 2 then eventually the JVM will encounter an OutOfMemoryException as the map does not seem to release a pointer to the conversation model.

This is quite a valid assumption? Or am I going mad after a long day at work?

OutOfMemoryException

You're not going mad. Yes, this is a weakness of the conversation model, as I've implemented it. I've thought about extending this model to timeout the obviously dead conversations. I just haven't gotten around to it.

Mark

OutOfMemoryException

That's excatly what I need!

Can you give me a hint on how to implement the timeout?

Thanks!

Springwebflow ?

Hi,
thanks for sharing this example ...
how does this solution compare with SpringWebflow ?
thanks!

Why do none of your posts list the files it mentions?

I like your posts a lot, and often they have files attached, supposedly, but I never see the links.. why is that?

Arrgghh! Fixed

Sorry, I didn't have anonymous viewing of attached files enabled.

Mark

Regarding Scope of variables around Struts.xml

I am facing a scenario which requires to have scoping only for struts.xml.

For example, When action leaves from One struts.xml to another, the variables should be descoped and retain null value when it enters the another struts.xml