|
JavaScript Web Flow TutorialPrerequisitesYou 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 applicationIn 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:
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:
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 viewsWith 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):
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 allBy 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 conceptsState storageBy 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. IncludesThere 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 dataThe 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 demarcationIf 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 blocksIn 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 factoriesRhino 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 stateYou 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:
bindStubProvider
and bindStubResolver methods on the HttpSessionFlowStateStorage .
By using these objects, you can provide the stubbing functionality for
any further application-specific objects.
|