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:
Next, the rhinoInSpring-servlet.xml. In its very simplest form, this is all you need to add to it to get the default functionality:
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:
(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):
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):
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!
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.
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.
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).
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.
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
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: