About
  What is it?
  Available components
    JavaScript Web Flow for Spring
      Webflow tutorial
  Download
  JavaDoc
  Licensing
  Who we are
SourceForge project page

JavaScript Web Flow Tutorial

Prerequisites

You are expected to be familiar with Spring MVC and JavaScript. To build the example, you need to download the Rhino in Spring distribution, and build it running "ant example" from its root directory. You will find the built web application inside the build/example directory.

Configuring the web application

In its simplest setup, you need to add an instance of the Spring's default dispatch servlet to your web application, just as you would with any Spring based web application. The relevant section of our example web.xml looks like this:

<servlet>
    <servlet-name>rhinoInSpring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>rhinoInSpring</servlet-name>
    <url-pattern>*.js</url-pattern>
</servlet-mapping>

This sets up a Spring dispatcher servlet to be configured from the rhinoInSpring-servlet.xml bean factory and to have all requests ending with .js forwared to it (since our controllers are JavaScript programs) - a quite standard setup for a Spring controller.

Next, the rhinoInSpring-servlet.xml. In its very simplest form, this is all you need to add to it to get the default functionality:

  <bean name="/*" class="org.szegedi.spring.web.jsflow.FlowController"/>

You will of course need to include some view resolver as well (in our example, we used the FreeMarker template engine for views, because we're JSP illiterate. You can of course use JSP, Velocity, or any other view technology supported by Spring.

There is also a strong chance that you will need to keep the Rhino context used by the controller for the duration of view rendering. If you used Spring with Hibernate, you are already familiar with the concept - it is named "OpenSessionInViewInterceptor" there, and our equivalent is "OpenContextInViewInterceptor". To add it to your dispatcher XML, you need to copy these lines:

<bean name="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
  <property name="interceptors">
    <list>
      <bean class="org.szegedi.spring.web.jsflow.OpenContextInViewInterceptor"/>
    </list>
  </property>
</bean>

(Of course, if you have a more complex Spring application, you might already have interceptors set up - then just add this one to the list).

Writing scripts and views

With this setup, you can place a .js file anywhere in your web application, and if you point your browser at it, it'll start running. Let's take a look at calculator.js in the example directory. It looks like this (with interesting pieces in red):

var tape = new Array();
tape[0] = 0;
for(;;)
{
    respondAndWait("calculator", { tape: tape });
    var operand1 = tape[tape.length - 1];
    try
    {
        var operation = 
            request.getParameter("operator") + " " + 
            request.getParameter("operand");
           
        tape[tape.length] = "  " + operation;
        tape[tape.length] = eval(operand1 + " " + operation);
    }
    catch(e)
    {
        tape[tape.length] = "  Error: " + e.message;
        tape[tape.length] = operand1;
    }
}

This is basically an infinite loop that keeps adding numbers to a list named "tape", thus emulating an old type calculator that kept the trail of the whole calculation on a roll of paper. The respondAndWait() function will send a response to the user's browser. Here, we're telling it to send back the view named "calculator" (this gets mapped by our FreeMarker view resolver to "calculator.ftl", but if you used JSP, it'd get mapped to "calculator.jsp"), and send it a data model - a map, essentially - with a single element named "tape" whose value is the tape variable. Then the respondAndWait() will stop and wait for the next request to come in from the browser. The currently processed HttpServletRequest object is always available in the "request" variable. Let's take a look at the view now. (Apologies if you don't read FreeMarker Template Language, but there really isn't much of it in there, it's mostly HTML):

<html>
  <head>
    <title>Tape calculator</title>
  </head>
  <body>
    <center>
    <h1>Tape calculator</h1>
    <p>Emulates a calculator that prints each operation on a tape. Enter an 
    operation and a number. You can use the browser's back button
    to step back as far as you want and continue from there, or you can 
    duplicate browser windows to split the calculation into multiple independent
    calculations as you wish.</p>
    <hr>
    [#foreach item in tape]
    <p>${item}</p>
    [/#foreach]
    <hr>
    <form method="POST" action="calculator.js">
      <input type="hidden" name="stateId" value="${stateId}">
      <table border="0">
        <tr><td>Operation:</td><td>
          <select name="operator">
            <option>+</option>
            <option>-</option>
            <option>*</option>
            <option>/</option>
          </select></td></tr>
        <tr><td>Number:</td><td><input type="text" name="operand"></td></tr>
        <tr><td colspan="2"><input type="submit" value="Calculate"></td></tr>
      </table>
    </form>
    </center>
  </body>
</html> 

First, you see a FreeMarker "foreach" directive that'll list all the items in the tape on a separate line. Our flow controller takes proper care to let the views see JavaScript native arrays as Java Lists and JavaScript native objects as Maps. JavaScript numbers and booleans show up as Java Double and Boolean instances, JavaScript strings as Java Strings. It is all rather intuitive. Also, if you somehow create or get a Java object inside your script and pass it to the view, it'll go through unchanged. Next, we see how you are expected to create the next request. You see that we are sending to the same script - the action of the form element is the same "calculator.js". However, there is a hiden field named "stateId" whose value is passed to the view as a data model variable with the identical name (hence ${stateId} will render it in FreeMarker into the output HTML page). This state ID is what connects this page with the script state on the server. Also, the script uses two request parameters named "operator" and "operand" , they're created as a drop-down select and a text field respectively. When the user submits the request, the script state on the server wakes up and returns from the respondAndWait() call. Let's continue examining the script now.

Now, it is stunningly simple, really. It retrieves the two request parameters, builds a string concatenating the last entry on the tape with the new operator and new operand, and uses JavaScript eval() function to evaluate the new expression. Then it appends the new operation as well as the result to the end of the tape, and loops back to respondAndWait(). If an error occurs, it appends the error message to the tape, and also appends the last good value. That's all!

The real fun begins - Running it all

By now, you have hopefully built the example, and mapped the build/example directory to your servlet container of choice. Go and point your browser at the calculator.js, and enter few values through several request/response cycles.

Now the fun begins.

Go back a few steps using your browser's back button, and continue from there. It works from the point where you backed to. Note: you didn't have to take care of synchronizing the browser state and the server state, by providing custom "back" links or buttons. Your users can freely use the browser's back button.

Open a new window from an existing state ("File->New Window" in IE or Fireox, or "Window->Duplicate" in Opera), and continue from there. Now return to the original window and contine from there. See how each window correctly has its own state - common up to the point of the duplication, but separate after that. Play with the back button and new windows freely as you wish, and see how the server always catches up with the correct state in each window, no matter how many times you split the calculation in two again (by duplicating a window) or roll back the calculation (by going back with the browser's back button).

And now realize that you didn't do anything special to support the user wandering through your webapp in multiple windows, going back and forth in them. For all you know, you just coded a single, linearly executing infinite loop!

Advanced concepts

State storage

By default, the flowstates between two requests are stored in a map that is bound to the HTTP session. This is implemented by the class HttpSessionFlowStateStorage but you needn't specify it as it is the default created by the flow controller when no other is found in the dispatch XML. By default, the states for the 100 most recent request for every session are being stored.

Another option is to store the states in a JDBC database. For this to work, you will have to explicitly create an instance of JdbcFlowStateStorage. There is a commented-out one in the dispatch XML of the example web application - you can use it to get started, although you'll probably want to replace the data source with some connection pooling implementation such as Apache DBCP for a real-world application.

Yet another option is to store the states in the HTML page generated in the response. This is achieved by using an instance of ClientSideFlowStateStorage. In this case, the stateId variable available to views is not an ID at all, but rather it is a full (textually encoded) representation of the state! It achieves unparalleled scalability, as zero state has to be stored on the server! Of course, entrusting the client to store the state can have potential security implications, so we provide full support for compression, encryption, and digital signing of the flow states to prevent the clients from tampering with them - you can employ any, all, or none of these features, of course. You can find several commented examples in the example dispatcher XML for it. One thing to pay attention to is that you should really use HTML forms with POST method to send the next request to the server with client-side state storage - you don't want few kilobytes worth of an encoded state showing up in the query string of the URL.

Includes

There is a built-in function named include() that takes one string as a parameter, interprets it as a path to a script, and executes that script in place. It is very handy for creating libraries of reusable functions that you can include from any other script. A path starting with / character is interpreted as an absolute path (relative to the root of the utilized resource loader, that is), while a path starting with any other character is a path relative to the including script, and can start with any number of ../ components to ascend to parent directories.

Sharing data

The natural way to share data across different flowstates is the progression of local variables from one flowstate to the one(s) that continue(s) it. All variables are deeply copied from one state to the other, by virtue of whole state being serialized and then deserialized, so i.e. changing an array element in a later flowstate doesn't affect the earlier flowstate at all - if you go back to it using the browser's back button and progress from there, it'll be as it ever was. There are situations where you need to share data with other flowstates, either in the same or in different scripts. I.e. backing to and continuing from halfway of a checkout process in a webshop after it was already completed once might be a bad idea. When writing such an application, you'll want to check after each wakeup from respondAndWait() whether certain assumptions still hold (i.e. basket is not yet purchased). For this, you can use any of the usual venues: HTTP session attributes, servlet context attributes, beans in the Spring application context, or maybe the best of all, queries against a relational database (either JDBC or some ORM, say Hibernate).

Context demarcation

If you're using interceptors or servlet filters to govern JDBC connection, Hibernate session, or some similar resource's lifetime, you must be aware when you code your scripts that your connections/sessions/etc. will get closed (flushed/committed/rolled back based on how everything's externally configured) whenever you invoke wait() or respondAndWait(), and newly reopened when the call returns with a new request. Just remember to structure your code accordingly.

finally blocks

In Rhino, all open finally blocks execute even when the execution temporarily suspends because of a wait() or respondAndWait() invocation. To help you differentiate in your finally blocks between the real final execution of the block and the "bogus" one (it has its usages, though) there is a isGoingToWait() function that returns true when the flow is going to wait, and false when it is not.

Using custom Rhino context factories

Rhino allow many aspects of its operation to be customized using a ContextFactory object. Rhino has a singleton global ContextFactory, but we discourage its use, as any global singletons are by their nature bad from the maintenance perspective. Rather, you can install a ContextFactory into either OpenContextInViewInterceptor or into the FlowController.

Clustering with Terracotta

Starting with version 1.2, Rhino-in-Spring's HTTP session storage is fully clusterable using Terracotta. The distribution includes a rhinoInSpring-tc-config-1.2.jar that is a Terracotta configuration module - you just need to reference it from your Terracotta configuration file, and it is ready to go. No further setup is necessary.

What's in the serialized state

You might wonder what is stored in the serialized flowstate. In short, every variable that is reachable from the script's global scope, as well as all local variables in all call frames currently on the stack. There are few exceptions though. The system recognizes several objects that are considered shared, and during serialization these objects are replaced with stubs. Then, when the flowstate is deserialized (because it received the next request), the stubs are resolved back to the shared objects, avoiding duplication. The following objects are stubbed:

  • the application context object
  • all named beans in the application context object and its ancestors
  • all objects representing JavaScript functions
  • all standard JavaScript global objects (Number, String, RegExp, etc.)
This means that even if you keep a reference to, say, applicationContext.getBean("foo") in a local variable between waits, it won't get duplicated, but rather it won't be serialized at all and will be rebound to the correct bean next time it is deserialized. However, it is important to notice that no other objects are stubbed, including the following:
  • HTTP session or its attributes
  • HTTP servlet context or its attributes (although the variable "servletContext" is specially managed - it'll work okay, but don't assign it to a variable with some other name)
  • Any other objects that you can obtain from any of the otherwise stubbed object, i.e. application context's parents, or unmanaged objects in general returned by various methods of stubbed beans. I.e. in
    var x = applicationContext.getBean("foo").getBaz()
    the object bound to x won't be stubbed. Notice it's okay for it to merely exist within the "foo" bean - as the "foo" bean is stubbed, serialization graph will stop at it and none of its internal objects will get serialized either. You just shouldn't forget a reference to it in a local variable, as that new reference will cause it to become directly reachable beyond stub and serialized. In best case, this'll lead to duplication of objects upon deserialization. In worst case, the newly reachable object isn't serializable at all and throws an exception.
However, starting with version 1.2, you can add application-specific stub providers and resolvers to the HTTP session, using the bindStubProvider and bindStubResolver methods on the HttpSessionFlowStateStorage. By using these objects, you can provide the stubbing functionality for any further application-specific objects.