Web State Management Using Externalized Dynamic Scoping
Once upon a time I asked for better scoping in web applications. Here is a possible way to do it. The idea is to use a system based on dynamic scoping -- the type of scoping you get in elisp, for example. What we see most, now, is lexical scoping. Java, C, Ruby, Common Lisp, Python, etc all use lexical scoping. Lexical scoping says that things are in scope based on where they are defined lexically. It is so fundamental that the idea of alternatives seems kinda weird. Well, there are -- but they have proven less useful in general. One of those is dynamic scoping. That is scoping such that variables down the call stack are in scope rather than what is in the source code. Emacs Lisp (elisp) works this way.
Illustration 1 shows an example of a scope stack. The box at the bottom represents the root, or base, of the stack. Additional frames are pushed onto it. A given variable is stored in a specific frame. The first variable we store is a
set to 7
. It is stored in stack frame #1. We push another frame, and store b
in that frame. Now anything using frame #2 for variable resolution will see both a
and b
. In stack frame #3 we reassign b
. The new value is said to mask the old value. Dereferencing b
in stack frames #3 or #4 will give you 8
, but the previous frames still retain the old values.
You can think of a series of web requests as traversing up this stack. Each request pushes another frame on the stack, and any new state for that request is stored in that frame. The accumulated state of the flow of requests is available to the current request. Use of the back button pops a frame off the stack, moving you back to the previous state -- and the values stored at the time you were originally at that point.
Even nicer, it allows you to fork the stack by opening multiple tabs (or windows for you IE users ;-) and continuing along. Illustration 2 shows a forked stack. Because all application state is dereferenced from the point of view of the current frame, you will maintain a correct view for a given path. You have two logical shopping carts as soon as you add something to it in a different window. Cool!
Nice in theory, but does it work? I put together YAWF to see how it works in practice. At its heart, YAWF is just a registry which can record and repopulate component state. Given this component:
package org.skife.sample;
import org.skife.yawf.State;
public class Hello extends Greeting
{
private String name;
public Hello()
{
this.setName("[Name]");
this.setSalutation("[Salutation]");
}
@State
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
}
YAWF state works on JavaBean properties. Any property can ask to have its state managed by annotating the getter as above. It requires that you have a getter and setter, though they don't have to be proper JavaBeans syntax if you are willing to provide a BeanInfo per normal JavaBeans stuff. Anyway, on to the fun stuff, let's manage some state. We'll start simple:
public void testBasicRecording() throws Exception
{
Frame frame = new Frame();
Hello hello = new Hello();
hello.setName("Brian");
frame.store(hello);
Hello another = new Hello();
frame.populate(another);
assertEquals("Brian", another.getName());
}
public void testStoredInParentFrame() throws Exception
{
Frame frame = new Frame();
Hello hello = new Hello();
hello.setName("Brian");
frame.store(hello);
Frame child = frame.createChild();
Hello hi = new Hello();
child.populate(hi);
assertEquals("Brian", hi.getName());
}
Nothing fancy here, you can see the rest of the tests and whatnot to make sure it abides by the rules I described in the TestFrames class. Nifty.
Now, because frames basically store name-value pairs, we have to assign a name to things. The default name for a field is the fully qualified name of the class declaring the getter, follows by a hash mark, followed by the property name. If you don't like that naming convention you can assign any arbitrary name to a piece of state, such as in the Greeting class.
Now, as it stands this is just a general state registry kind of thing, let's make it work for a web app. To do that we introduce a mechanism to serialize out a frame, the FrameEncoder. This one does a bare-bones level of encoding -- it serializes the frame, gzips it, encrypts it, base 64 encodes, then url encodes. This gets the size to something reasonable, and lets us stick the whole stack on the url. It could be optimized further by flattening the stack before serialization, doing custom serialization of the value storage system, etc, but the gzipped serialization was enough for this =)
Once we can serialize our (state) stack out, we need a convenient way to play with it from the web. A simple servlet filter and a couple associated utility classes do the job so that we can have a small sample app in a jsp. The important stuff in this JSP (yes, with scriptlet, is is just a sample) starts with the stack management:
<jsp:useBean id="hello" class="org.skife.sample.Hello" scope="request"/>
<%
Frame frame = Framing.getFrame(request);
frame.populate(hello);
if (request.getParameter("name") != null)
hello.setName(request.getParameter("name"));
if (request.getParameter("salutation") != null)
hello.setSalutation(request.getParameter("salutation"));
frame.store(hello);
%>
Which just records some form parameters. Setting them is likewise pretty simple:
A Form:
<form action="<%= response.encodeURL("index.jsp") %>" method="post">
<label for="name">Name </label>
<input id="name" type="text" name="name" size="40"/>
<input type="submit" value="Change Name"/>
</form>
The key thing is to make sure to always URL encode the links so that the stack can be copied into the url =) Now you carry around, on each link, a copy of the stack (~350 characters usually, after gzipping). The application state is encrypted (algorithm and key configured in the web.xml) to prevent meddling, and you have no http session to worry about replicating. You have a seperate stack of state information for each flow. You a correctly behaving back button. Pretty nice.
YAWF is really a proof of concept, but is a pretty decent one. There are a ton of optimizations that can still be made, but it has proven to me, anyway, that the concept has some legs. If you want to play with it, I have the sample app (the single jsp anyway) set up so that you just need to untar, and ant sample
to start it up. It does require jdk 1.5 (note the annotation usage). Have fun!