Introduction
A non-trivial single page application will have to manage state (model) manipulation and implement view navigation between states. Although it is technically possible to do this on the client's side, i.e. with JavaScript and local storage introduced by HTML5, this might not be the optimal approach. Considering that a typical application will already have some sort of heavy-duty logic implemented in Java on the back-end, we may, as well, take advantage of the existing flow management frameworks available for Java to maintain state information and conversation logic needed to implement a relatively complex view navigation.
One such excellent framework is Spring Webflow (SWF). It allows for declarative style specification of view-states, action states, decision states and transition between them based on the events (request parameters). It also makes available a hierarchy of scopes: session, flow, flash, request, view where to store the flow related variables (state shared by the views).
The idea behind the proof-of-concept application discussed in this article is to show how one can use SWF with a simple SAPUI5 application which has following characteristics: it's a single-page application, i.e. one HTML page with only AJAX calls (no page reloads), starting state ("step1") allows transition to the second state ("step2") with some user-defined parameters, there are two transitions from the second state, back to the first state and to the finish state ("done") which ends the flow.
Spring Webflow Setup
We'll be creating a simple Maven-driven SWF application (see the examples from sprig-webflow-samples for typical setup) as a starting point. Note that SWF already includes the Dojo library for decorating front-end controls as widgets (in itself a wonderful front-end framework). But, since we are using SAPUI5 Javascript framework, we can exclude the artifact containing Dojo distribution from our SWF dependency (artifact id: "spring-js-resources"). Below, there is a general overview of the application's files.
There is a Spring root application context, a context for Spring MVC and SWF beans loaded by the DispatcherServlet (mvc-dispatcher-servlet.xml). SWF is setup as usual with the following definition of the required flow registry and flow handler adapter beans (other beans are omitted).
<webflow:flow-registry id="flowRegistry"> <webflow:flow-location id="demo" path="/WEB-INF/flows/flow.xml"/></webflow:flow-registry><bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"> <property name="flowExecutor" ref="flowExecutor"/> <property name="ajaxHandler"> <bean class="ch.unil.demo.swf.JsonHandler"/> </property></bean>
The ID of the flow will be used to start the flow by issuing the initial AJAX call to "/demo" URL. SWF allows specifying a custom implementation of AjaxHandler interface which decides whether to treat each request as an AJAX request. Our implementation simple looks for the "application/json" content type.
public class JsonHandler implements AjaxHandler { @Override public boolean isAjaxRequest(HttpServletRequest request, HttpServletResponse response) { boolean isAjax = false; if (request.getContentType().equals("application/json")) { isAjax = true; } return isAjax; } @Override public void sendAjaxRedirect(String targetUrl, HttpServletRequest request, HttpServletResponse response, boolean popup) throws IOException { } }
The flow itself is quite simple. It defines two view-states and one end-state, as well, as the relevant transitions.
<view-state id="step1" view="/WEB-INF/flows/view_model_json.jsp"> <on-entry> <evaluate expression="viewModelController.updateViewModelData(flowRequestContext)"/> </on-entry> <transition on="next" to="step2"/></view-state> <view-state id="step2" view="/WEB-INF/flows/view_model_json.jsp"><on-entry> <evaluate expression="viewModelController.updateViewModelData(flowRequestContext)"/></on-entry><transition on="finish" to="done"/><transition on="back" to="step1"/></view-state><end-state id="done" view="/WEB-INF/flows/done.jsp">
All of the state management logic is encapsulated within a POJO, ViewModelController, which updates three view-scope variables upon each view-state entry, depending on the current state and the transition event. The variable "flowExecutionUrl" contains the URL with the execution key for the current state (it is automatically available in any JSP-backed view in SWF). This is the URL to which all of the AJAX requests should be sent, The variable "stateId" identifies the current state of the flow ("step1", "step2", "done"). The variable "modelData" contains a JSON data used to construct a JSONModel for the dynamically created JSView of the SAPUI5 front-end. The data is serialized using Jackson ObjectMapper object wired into the controller from the root application context. The POJOs: StepOne, StepTwo, and Done (extensions of ModelData) are simply used to serialize JSON string containing model data.
public class ViewModelController { private ObjectMapper jacksonMapper; public void setJacksonMapper(ObjectMapper jacksonMapper) { this.jacksonMapper = jacksonMapper; } public void updateViewModelData(RequestContext requestContext) throws IOException { ModelData modelData = null; // get the current state String curStateId = requestContext.getCurrentState().getId(); // create or update model data for the view based on the current state if (curStateId.equals("step1")) { StepOne data = new StepOne(); if (requestContext.getCurrentEvent()!=null){ // back from step2 data.setText("Back from step 2. Enter a value for the output parameter."); } else { // flow start data.setText("Start of the flow. Enter a value for the output parameter."); } modelData = data; } else if (curStateId.equals("step2")) { StepTwo data = new StepTwo(); data.setText("Parameter from step 1: " + requestContext.getRequestParameters().get("outputParam")); modelData = data; } else { // end-state Done data = new Done(); data.setText("The end-state: done. Goodbye."); modelData = data; } // serialize model data as JSON stirng and store it in the view scope variable requestContext.getViewScope().put("modelData", jacksonMapper.writeValueAsString(modelData)); } }
Each of the two flow's view-state uses a generic JSP (veiw_model_json.jsp) to expose the view-scoped variables as JSON content returned with each AJAX call.
{ "flowExecutionUrl": "${flowExecutionUrl}", "stateId": "${flowRequestContext.currentState.id}", "modelData": ${modelData} }
SAPUI5 front-end
On the front-end side, we have a standard SAPUI5 web application set up, with the default "index.html" point of entry. Note that for the root application path mapping to work as expected (for URL paths like "/" or "/index.html") you should have the following declaration in your Spring MVC setup.
<mvc:default-servlet-handler/>
This uses a helpful "mvc" namespace to setup forwarding to the container's default Servlet. The welcome file is straight forward. It contains the standard bootstrap script for the SAPUI5 libraries (which, of course, should be available in the "/WEB-INF/lib" folder), the main executable script "app.js" for the application logic, and the usual div with "content" ID attribute where each view's controls will be placed at. Here is "app.js" file.
var flowExecutionUrl = "/demo?mode=embedded", currentView, header; function ajaxCall(eventId, params, context) { if (!params) { params = {}; } if (!context){ context = this; } if (eventId) { params._eventId = eventId; } return jQuery.ajax(flowExecutionUrl, { type: "GET", data: params, dataType: "json", contentType: "application/json", cache: false, context: context }); } function updateMvc(executionUrl, stateId, modelData) { var controller, model, view; // remove old view, if needed if (currentView) { currentView.destroy(); } // update current flow execution url for successive calls flowExecutionUrl = executionUrl; // create controller for the current state controller = sap.ui.controller("demo." + stateId); // create new JSON model using the provided model data and register it with the controller model = new sap.ui.model.json.JSONModel(); model.setData(modelData); controller.model = model; // create a view for the current state view = currentView = sap.ui.view({ type: sap.ui.core.mvc.ViewType.JS, viewName: "demo." + stateId, controller: controller }); view.placeAt("content"); } // register custom module path jQuery.sap.registerModulePath("demo", "js/demo"); // execute an ajax call to start the flow and display the view for the start state ajaxCall().done(function (response) { updateMvc(response.flowExecutionUrl, response.stateId, response.modelData); }); // create application header header = new sap.ui.commons.ApplicationHeader({ logoText: "Demo: Spring Webflow and SAPUI5", displayWelcome: false, displayLogoff: false }); header.placeAt("header");
Couple of things to notice here. First, the URL for the initial AJAX call (see "flowExecutionUrl" variable declaration) to start the flow should mach the ID of the flow ("/demo"), as we have stated above. Also, since we are dealing with AJAX driven application, we need to start the flow in the embedded mode (hence the query parameter. This (together with our custom AjaxHandler) set's up the SWF to properly handle AJAX requests without issuing it's usual redirects which allows for "the partial page updates."
The script declares a helper function which uses jQuery's "ajax()" functionality to communicate with the SWF by sending "GET" requests (note that "POST" requests do not work with this setup) to the current flow execution URL with the given "_eventId" parameter. Another helper function, "updateMvc()", is intended to be called upon a successful resolution of the deferred returned from the AJAX call. It updates the current flow execution URL (to reflect the state change), creates new controller and JSView objects by appending the state ID to the custom "demo" module, so that, for example, if "stateId" is "step1" (as declared in "flow.xml") the controller created will reference the "demo/state1.controller.js" file, while the corresponding view is described in the "demo/state1.view.js" file. Once the controller is instantiated, but before the creation of the view, we can pass an instance of JSONModel to it to be bound to the view during the "onInit" phase. The model is created with the "modelData" object retured from the SWF for the given state.
The views and controllers are all pretty standard, just calling the helper functions during UI event handling. For example, when user clicks "Next" button on the first view (view-state "step1"), the controller executes the following code.
next: function (outputParam) { ajaxCall("next", { outputParam: outputParam }).done(function (response) { updateMvc(response.flowExecutionUrl, response.stateId, response.modelData); }); }
Observing the application at the runtime
When the root path of the application is accessed at "/index.html" an AJAX call to "/demo?mode=embedded" is executed. We can see that the flow is started with the new execution for the current client and the initial state ("e1s1").
When the "Next" button is clicked, the application transitions to the state "step2" (the call is executed with "_eventId=next" query parameter).
Conclusion
Using Spring Webflow to manage state information (creating and updating JSON model used in views), as well as, declaratively specifying state transitions based on well defined events in the single-page SAPUI5 application can be an interesting solution, especially if the application already requires the use of Java on the server side. One possible approach to combining SAPUI5 MVC and SWF, described in this article, is to expose view-scoped variables (state ID, model data) as JSON values via a generic JSP used in the view-states of the flow. The generated JSON, then, is used as a response to the AJAX calls to the execution flow URL with transition event IDs and output parameters specified as query parameters. Upon receiving the response from the server, the application should instantiate an controller and a view with appropriate names based on ID of the current state. The model data from the response can be used to create a JSONModel which can then be registered with the controller and bound to the view.
We have only scratched the surface of what Spring Webflow can offer. Its main advantage is that it provides a clear, declarative approach to state definition and management. But since it plugs in well with the rest of Spring's driven architecture there are multitude of other functionalities available to the programmer, once the decision to use SWF had been taken. To name a few: there is Spring Security for securing flows, using Spring EL in flow definitions, bean validation, unit testing the flows and more.