-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial
There is a tutorial that walks you through the details of the examples.
You are expected to be familiar with Spring MVC and JavaScript. To build the example, you just need to run mvn jetty:run
from its root directory. You can refer to Jetty Maven Plugin documentation for additional information about building a deployable web application etc.
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>spring-webmvc-jsflow</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>spring-webmvc-jsflow</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
This sets up a Spring dispatcher servlet to be configured from the spring-webmvc-jsflow-servlet.xml
bean factory (servlet name + -servlet.xml
) 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 spring-webmvc-jsflow-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 either JSP 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).
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 line marked with a comment):
var tape = new Array();
tape[0] = 0;
for(;;) {
respondAndWait("calculator", { tape: tape }); // THE INTERESTING LINE!
var operand1 = tape[tape.length - 1];
try {
var operation =
<font color="red">request.getParameter("operator") + " " +
request.getParameter("operand");</font>
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] [#-- INTERESTING LINE 1 --]
<p>${item}</p>
[/#foreach]</font>
<hr>
<form method="POST" action="calculator.js"> [#-- INTERESTING LINE 2 --]
<input type="hidden" name="stateId" value="${stateId}"> [#-- INTERESTING LINE 3 --]
<table border="0">
<tr><td>Operation:</td><td>
<select <font color="red">name="operator"></font>
<option>+</option>
<option>-</option>
<option>*</option>
<option>/</option>
</select></td></tr>
<tr><td>Number:</td><td><input type="text" name="operand"></td></tr> [#-- INTERESTING LINE 4 --]
<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 at this point...
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!
Go and point your browser at http://localhost:8080/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 ("duplicate page" or somesuch in most browsers), 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. Duplication effectively forked the execution in two separate executions that are now independent from each other!
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 org.szegedi.spring.web.jsflow.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 org.szegedi.spring.web.jsflow.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 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 org.szegedi.spring.web.jsflow.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.
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
.
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 alland 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
the object bound to
var x = applicationContext.getBean("foo").getBaz()`
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, you can add application-specific stub providers and resolvers to the HTTP session, using the bindStubProvider
bindStubResolver
methods on the HttpSessionFlowStateStorage
. By using these objects, you can provide the stubbing functionality for any further application-specific objects.