State Management
This document has two major sections:
- #Smart State Management using the ActionBeanContext
- #Redirect-after-Post using the Flash Scope
Smart State Management
In this section we'll walk through a strategy for how to access HttpSession, Cookies and other HttpServletRequest/Response behaviours in a way that's type safe and keeps your ActionBean classes testable (woohoo!). The key to this is providing your own ActionBeanContext subclass.
Subclassing ActionBeanContext
The following is an example subclass that allows for type safe access to a User object stored in session:
public class MyAppActionBeanContext extends ActionBeanContext { public void setUser(User user) { getRequest().getSession().setAttribute("user", user); } public User getUser() { return (User) getRequest().getSession().getAttribute("user"); } }
Now that you have your own ActionBeanContext you'll need to tell Stripes to use it. The easiest way is to drop your class into an extensions package (see Extensions). Alternatively, you can add the following initialization parameter to the Stripes Filter in your web.xml:
<init-param> <param-name>ActionBeanContext.Class</param-name> <param-value>com.myco.MyAppActionBeanContext</param-value> </init-param>
Using your new ActionBeanContext
Now, the ActionBeanContext that gets set on your ActionBean will always be a MyAppActionBeanContext. You'll still have to cast the context object that you receive in your ActionBean, but thanks to a new feature in Java 1.5 called co-variant return types, you can at least do it once, and not litter your code with casts.
public class MyActionBean implements ActionBean { private MyAppActionBeanContext context; /** Interface method from ActionBean. */ public setContext(ActionBeanContext context) { this.context = (MyAppActionBeanContext) context; } /** Interface method from ActionBean, using a co-variant return type! */ public MyAppActionBeanContext getContext() { return this.context; } ... }
In Java 1.4 and earlier, the getContext() method would not compile because the interface specifies it must return ActionBeanContext, and the class specifies MyAppActionBeanContext. It'd fail even though MyAppActionBeanContext extends ActionBeanContext. In Java 1.5 this is allowed, and it helps make things a bit cleaner. And if you create your own BaseActionBean class you can put this in there and never have to see it again
Now that you have your own ActionBeanContext class you can
- Add type safe methods to access things you need to store in session (hopefully not too many things)
- Add type safe methods for getting and setting the values of cookies
- Put all the strings/keys needed for the above operations in one place
- Access methods and functionality of the request and response objects without coupling your ActionBeans to them
But wait, there's more. Because the ActionBeanContext is attached to your ActionBean, and your ActionBean is dropped into a request attribute, you now have two ways of accessing things stored in session. On a JSP:
<div>${user} == ${actionBean.context.user}</div>
While this may seem trivial, there's one benefit. The first syntax ${user} assumes that you have placed the User object in to a JSP scope (e.g. session) under the attribute key "user". If you ever change that key, say to "__secretPlace_user", your JSPs will break. You're probably less likely to change the type safe methods on your ActionBeanContext, so the ${actionBean.context.user} might be safer. Not a huge benefit, but worth noting.
Enabling Testing
It's often difficult to test classes that live in the web tier. This is usually because they either need to interact with one of the Http* objects, or they are forced to through the API of the Servlet/Action/Whatever model being used. Unit testing of Stripes ActionBeans is already pretty simple (just instantiate, set context, set properties and go), but it falls down when your ActionBean really needs to interact with the request or the session. Use those and suddenly testing is difficult...you might have to have a set of mock Http* objects, etc.
With just a little bit more work, you can make your ActionBeans completely testable. Using our MyAppActionBeanContext example above, we can create an abstract class that our ActionBeanContext will extend, and which our ActionBeans will use. Let's refactor a bit:
public abstract class MyAppActionBeanContext extends ActionBeanContext { public abstract void setUser(User user); public abstract User getUser(); }
Our "real" class would look much the same as before, but perhaps this time we'd name it differently:
public class MyAppActionBeanContextImpl extends MyAppActionBeanContext { public void setUser(User user) { getRequest().getSession().setAttribute("user", user); } public User getUser() { return (User) getRequest().getSession().getAttribute("user"); } }
And then, we create out test ActionBeanContext:
public class MyAppActionBeanContextTestImpl extends MyAppActionBeanContext { private User user; public void setUser(User user) { this.user = user; } public User getUser() {return this.user; } }
Et voilà, we have a test rig for our ActionBeans that allows us to simulate the real environment with very little effort. You can imagine other ways of doing this. Perhaps the test version (MyAppActionBeanContextTestImpl in our example) could simply extend the "real" version (MyAppActionBeanContextImpl in our example) and override methods that access session or request. You could even go one further and have a protected helper method in your "real" context class called something like setInSession(String key, Object value), which in the real version calls getRequest().getSession().setAttribute(), but in the test version just puts the value in a local Map.
Redirect-after-Post
Redirect-after-Post is the name given to a fairly simple technique. The idea is that after a form POST has occurred and the server has done something significant (create a user, place a trade etc.) that it is safest to then redirect the user to another (or perhaps the same) page. By redirecting we ensure that if the user hits the refresh button they don't re-submit the form. If we were to have simply forwarded to a page after the POST, a refresh would re-post the form!
But there is a down side to redirecting. Anything stored in request attributes is lost when the redirect is issued (because the request for the page is not the same request). One option is to stuff things into session, but that's a bad idea(tm) because things will break when the user has more than one browser window open using your application.
The FlashScope
This is the problem that the FlashScope is designed to solve. In essence the flash scope is a scope much like request scope, session scope etc. Whereas the request scope is defined as existing for the lifetime of a request, the flash scope is defined as existing for the duration of this request and the subsequent request. Any item added to the flash scope will be made available as a request attribute during the current request, and during the next request.
An example of flash scope usage is non-error messages functionality in Stripes. The ActionBeanContext has a getMessages()
method that returns a list to which messages may be added. This list is stored in flash scope so that the messages are available to the current request (should the ActionBean forward to a page) and in the next request (should the ActionBean redirect to a page). The code looks like this:
public List<Message> getMessages(String key) { FlashScope scope = FlashScope.getCurrent(getRequest(), true); List<Message> messages = (List<Message>) scope.get(key); if (messages == null) { messages = new ArrayList<Message>(); scope.put(key, messages); } return messages; }
The true
on the first line tells FlashScope to manufacture a FlashScope if one doesn't yet exist. After that the scope is used just like any other scope - as a container of key/value pairs.
Flashing ActionBeans
By default ActionBeans are not put in flash scope. This is partly for backwards compatibility reasons, and partly because it just isn't necessary to do it all the time! That doesn't mean it's hard to do though. If you're using the standard RedirectResolution it's as simple as chaining one more method call:
return new RedirectResolution("/some/page.jsp").flash(this);
Even if you're redirecting some other way, it's still very easy:
// From within the ActionBean FlashScope.getCurrent(getContext().getRequest(), true).put(this);
Note: Only do this if you're redirecting to a JSP. If you redirect to an ActionBean that forwards to a JSP, the flashed ActionBean will be overwritten with the ActionBean you redirected to.
Accessing the FlashScope in your JSP
Lets say you called this method in one of your ActionBeans:
public void addRecipe(HttpServletRequest request, String message) { FlashScope fs = FlashScope.getCurrent(request, true); List<String> messages = (List<String>) fs.get("recipes"); if (messages == null) { messages = new ArrayList<String>(); fs.put("recipes", messages); } messages.add(message); }
Then you redirected to another ActionBean which forwards to a JSP. This is how you access the FlashScope in your JSP:
<c:forEach var="recipe" items="${recipes}"> <div class="recipe_name">${recipe.name}</div> </c:forEach>
As you can see, when you put an object into the FlashScope, it can be accessed directly by its key in an EL statement.
How FlashScope works
The FlashScope works by temporarily storing instances of itself in Session, and removing them on subsequent requests. As a result, when a FlashScope is used, an additional parameter is appended to redirect URLs to tell Stripes which FlashScope to use. Because of this parameter two or more browser windows (or tabs) in the same session will never get confused and access each others' FlashScopes.
An obvious side affect of this strategy is that occasionally FlashScopes get orphaned in session. For this reason Stripes will examine all FlashScopes in existence for a session each time a request is received; each flash scope starts "aging" when the request that generated it is completed, and any FlashScope beyond a certain age (currently 2 minutes) is destroyed. Think of this like session expiration on a smaller scale.
Bringing it all Together
Hopefully the above sections on smart state management and redirect-after-post made sense. The FlashScope actually provides the single best example to date of why using the ActionBeanContext to manage all state related logic is a good idea.
The FlashScope was introduced in Stripes 1.2. Prior to 1.2 the list of non-error messages was stored in a request attribute. Now that the flash scope is available the non-error messages are stored there. Making the change was trivial because ActionBeans were completely isolated from the details of how the messages were stored and retrieved. Had ActionBeans been accessing request (or session) directly the change could not have been made without breaking existing code.