Chariot Training Classes

Training Courses

I run Chariot's training and mentoring services. We provide training in AngularJS, HTML5, Spring, Hibernate, Maven, Scala, and more.

Chariot Education Services

Technology

Chariot Emerging Tech

Learn about upcoming technologies and trends from my colleagues at Chariot Solutions.

Resources

Chariot Conferences

Podcasts

Entries in webflow (5)

Saturday
Feb252012

Quizzo Saturday - testing web flows

GitHub commit URLs: 82add3e1e23eee8b84058c12fcd6ff211f70096e and more importantly, 4cfed580c3404e61d501ac4ee0182c294b1b0b89

Ok, this is kind of a tangent. But I wanted to really debug the flow logic for these interactions, and better understand the flow logic itself before I bother to write up nice looking views.

Test that our flow starts with the correct state

I started with this first step, which is pretty easy to test:



<var class="com.chariot.games.quizzo.web.flow.TeamSetupForm" name="teamSetupForm"/>

<view-state id="register-team" view="playQuizzo/register-team"
            model="flowScope.teamSetupForm">
    <transition on="continue" 
                to="register-team-members" 
                bind="true" validate="true"/>
</view-state>

This state fills in a portion of the teamSetupForm flow-scoped variable. How do we test this? Somewhat easily. First, we start by building a test that is a subclass of AbstractXmlFlowExecutionTests:

public class QuizzoFlowTest extends AbstractXmlFlowExecutionTests {

  @Override
  protected FlowDefinitionResource getResource(
               FlowDefinitionResourceFactory resourceFactory) {

    return resourceFactory.
        createFileResource("src/main/webapp/WEB-INF/views/playQuizzo/flow.xml");
  }
}

Next, we have to build a method to expose a stub version of our Web Flow Spring Bean, the QuizzoFlowManager. It's referred to in the rest of the flow as quizzoFlowManager. This stub will replace our Spring Bean, and will provide access to any methods the flow calls. The flow execution engine may throw errors if it looks for this bean while parsing the form. So, we add this method to the test:


@Override
protected void configureFlowBuilderContext(
               MockFlowBuilderContext builderContext) {

  StubQuizzoFlowManager stubQuizzoFlowManager = 
                        new StubQuizzoFlowManager();

  builderContext.registerBean("quizzoFlowManager", 
                        stubQuizzoFlowManager);
}

This method is called once we execute any flow start or resume methods in our mock flow engine.

Writing a flow test

Consider the test code below:

public void testStartFlow() {
  MutableAttributeMap map = new LocalAttributeMap();
  MockExternalContext context = new MockExternalContext();
  startFlow(map, context);
  assertCurrentStateEquals("register-team");
  TeamSetupForm form = (TeamSetupForm) getFlowScope().get("teamSetupForm");
  assertNotNull(form);
}

The test does the following things:

  • Sets up a attribute map, required for starting a flow. This is just a property map.
  • Builds a fake Flow context, also required when launching a flow.
  • Calls the base test class's startFlow method, which launches the flow.
  • Checks to make sure that the flow is waiting on the register-team view state.
  • Checks to make sure the Web Flow engine creates the variable, teamSetupForm in the var webflow tag.
  • Checks to make sure the form is not null.

But what about checking events? For that, we'll push a "continue" event, and set the values that the form would have submitted in our Form bean:

@Test
public void testSubmitTeamName() {
  setCurrentState("register-team");
  MockExternalContext context = new MockExternalContext();

  getFlowScope().put("teamSetupForm",
      createTeamSetupForm("The Jets", "When you're a Jet you're a Jet"));

  context.setEventId("continue");
  resumeFlow(context);
  assertCurrentStateEquals("register-team-members");
}

In this test, we start with the flow sitting on the register-team state. Since Web Flow is not running any states before our unit test, we have to create our teamSetupForm ourselves, and set the value of it using a helper method to include the team name and team message.

We then tell the context to send the continue event, and resume our flow. We check once this completes to make sure we are now sitting on the register-team-members state.

A more complex example

Now we are going to test the next state and its transition to the following state using continue:

<view-state id="register-team-members" view="playQuizzo/register-team-members"
          model="flowScope.teamSetupForm">

  <transition on="continue" to="ready-to-play" bind="true" validate="true">
      <evaluate expression="quizzoFlowManager.saveTeamData(flowRequestContext)"/>
  </transition>
  <transition on="add-team-member"/>
  <transition on="remove-team-member"/>
  <transition on="back" to="register-team"/>
</view-state>

<action-state id="ready-to-play">
  <evaluate expression="quizzoFlowManager.pollReady(flowRequestContext)"/>
  <transition on="yes" to="play-round"/>
  <transition on="poll" to="poll"/>
</action-state>

You may think this test executes just fine:

@Test
public void testSubmitTeamMembers() {
  setCurrentState("register-team-members");

  MockExternalContext context = new MockExternalContext();
  getFlowScope().put("teamSetupForm",
      createTeamSetupForm("The Jets",
              "When you're a Jet you're a Jet",
      "Ice", "Action", "Baby John", "Tiger", "Joyboy"));

  context.setEventId("continue");
  resumeFlow(context);
  assertCurrentStateEquals("ready-to-play");
}

But it fails on the state assertion. It states that the state is actually play-round. How can this be?

Action States

This is because we're using an action state from our web flow. Action states do not wait on anything. They immediately execute their evaluations, and take action from their responses. I knew this when building the flow, but scanned by it when writing the test. Ugh.

How did I figure this out? You'd think by closely reading the ready-to-play state definition, but no... Instead, I amped up the logging for the webflow framework by setting this entry in log4j.properties:

log4j.logger.org.springframework.webflow=trace

This rewarded me with a great amount of useful detail:

2012-02-25 12:19:47,032 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImplFactory - Creating new execution of 'flow'
2012-02-25 12:19:47,047 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Resuming in org.springframework.webflow.test.MockExternalContext@1494cb8b
2012-02-25 12:19:47,054 [main] DEBUG org.springframework.webflow.engine.Flow - Restoring [FlowVariable@3209fa8f name = 'teamSetupForm', valueFactory = [BeanFactoryVariableValueFactory@2d20dbf3 type = TeamSetupForm]]
2012-02-25 12:19:47,072 [main] DEBUG org.springframework.webflow.engine.ViewState - Event 'continue' returned from view [MockViewFactoryCreator.MockView@3e0d1329 viewId = 'playQuizzo/register-team-members']
2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
...
2012-02-25 12:19:47,084 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]]; result = success
2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@3a4c5b4 on = continue, to = ready-to-play]
2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'register-team-members'
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.engine.ActionState - Entering state 'ready-to-play' of flow 'flow'
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]]
2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]]; result = yes
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@47db9852 on = yes, to = play-round]
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'ready-to-play'
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.ViewState - Entering state 'play-round' of flow 'flow'
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,089 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]]
2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]]; result = success
2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Assigned key 1
2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.ViewState - Rendering + [MockViewFactoryCreator.MockView@643cb075 viewId = 'playQuizzo/play-round']
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState -   Flash scope = map[[empty]]
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState -   Messages = [DefaultMessageContext@4c6504bc sourceMessages = map[[null] -> list[[empty]]]]
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution.  As a result, the new state is 'play-round' in flow 'flow'
2012-02-25 12:19:47,093 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution.  As a result, the new state is 'play-round' in flow 'flow'

If you spend time reading this, you'll see two flow state executions - one for the view state, and one for the action state. Our stub class returns "yes" from our transition in the poller state, and so we move to the next view state, play-round

Wrap-up

Testing in WebFlow is rather difficult, because of the number of mock objects, the complexity of flow logic, and the fact that it's in XML, which is not compiled and therefore really requires tests. But is is worth it, as long as you're not using this as an excuse to test your beans themselves. You should test those either in unit or Spring integration tests. My assertions are mostly around making sure we submitted the right events to trigger the right view states.

Next I'll be wiring up some pages to these flow view states, and starting to really get the processing going. I'm also going to start writing an admin page to control the game itself and provide an audience view.

Thursday
Feb232012

Wednesday / Thursday Quizzo - flowin' it

Ok, most of my time in the last two days was non-quizzo related. Well, I should say non-business logic quizzo related. See, I was chasing my tail around the integration tests for the state of the quiz, which I've set up with a facade Spring Bean. Here's my interface:

public interface QuizRunStateMachine {
  void startQuiz(Long quizId, String text);
  boolean nextQuestion();
  Map<String, BigDecimal> getScores();
  Long getCurrentQuestionId();
  boolean submitAnswer(Team team, Answer answer);
  void endQuiz();
  QuizRun getQuizRun();
}

When I test the implementation of this state machine, which flips various bits in my database and keeps track of the current state, it works fine within IntelliJ (my IDE weapon of choice). However, when I use the maven test process, all tests fail due to the assertions coded within the bean. Grr.

I have no answer for that...

See above.

Anyhoo... Web flow, anyone?

Roo makes setting up the web flow engine easy. You just issue this simple command:

roo> web flow setup --flowName playQuizzo

The command sets up Web Flow, putting the flow itself in the WEB-INF/views/playQuizzo directory.

This blog entry lays out the beginning of using this strategy to implement a quiz player interface using web flow. It's incoherent a bit, as I'm sort of live-blogging this, so at some point I may go back and clean it up. But for now, hopefully it will serve as a helpful guide.

The flow

Here is a simple web flow (so far) that I've put together to handle the quiz flow:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/webflow     
   http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

  <var class="com.chariot.games.quizzo.web.flow.TeamSetupForm" 
        name="teamSetupForm"/>

  <view-state id="register-team" 
            view="playQuizzo/register-team"
           model="flowScope.teamSetupForm">
    <transition on="continue" to="register-team-members" 
       bind="true" validate="true">
      <evaluate expression="quizzoFlowManager.debug(flowRequestContext)"/>
    </transition>
  </view-state>

  <view-state id="register-team-members" 
            view="playQuizzo/register-team-members"
           model="flowScope.teamSetupForm">
    <transition on="continue" to="ready-to-play"/>
    <transition on="add-team-member"/>
    <transition on="remove-team-member"/>
    <transition on="back" to="register-team"/>
  </view-state>

  <view-state id="ready-to-play" view="playQuizzo/ready-to-play">   
    <transition on="go" to="play-round">
      
    </transition>
  </view-state>

  <view-state id="play-round" view="playQuizzo/play-round">    
    <transition on="submit-answer" to="play-round"/>
    <transition on="finish" to="game-over"/>
  </view-state>

  <end-state id="game-over" view="playQuizzo/game-over"/>

</flow>

This web flow is relatively simple- it requires the users to register their teams and team members, then keeps presenting the current question from the quiz run state machine, and as the submissions progress, the users keep submitting answers for each question, etc...

I haven't worked all of this out yet, and as I'm writing this blog post I'm about to commit at least a lame beginning to the scripts, but you get the idea.

Building our form views

Ok, so I have a love-hate relationship with the web tier of Roo. For simple cases, if you don't go off the rails (couldn't resist), it works fine. However, if you want to free-style it, maybe such as set up non-RESTful views such as web flows, it breaks down because of the implicit conventions (specifically the form has to be RESTful in some way, includes a primary key, optional version, and most importantly, those dang Submit buttons.

I'm using Web Flow for the quiz takers. It makes sense to me (so far) that the quiz manager will control the flow using a specific set of protected pages only he can get to, which interact with the manager methods of my state machine (starting a quiz, advancing to the next question, etc).

However, the users will be walking through a directed web flow. The flow will have various actions, and they will be whatever I want them to be - bottom line, I have to break with Roo conventions. So I'm setting up my own Roo tags for a generic form, button grouping, and buttons.

The Form

I will update this as I go along, but here is my current generic "form" tag, which I place alongside form:create and others as form:form:

<jsp:root xmlns:c="http://java.sun.com/jsp/jstl/core" 
          xmlns:fn="http://java.sun.com/jsp/jstl/functions"
          xmlns:util="urn:jsptagdir:/WEB-INF/tags/util" 
          xmlns:form="http://www.springframework.org/tags/form"
          xmlns:jsp="http://java.sun.com/JSP/Page" 
          xmlns:spring="http://www.springframework.org/tags" version="2.0">

    <jsp:output omit-xml-declaration="yes"/>

    

    <c:set var="enctype" value="application/x-www-form-urlencoded"/>

    <c:if test="${multipart}">
        <c:set var="enctype" value="multipart/form-data"/>
    </c:if>

    <c:if test="${empty action}">
        <spring:url value="${path}" var="action"/>
    </c:if>

    <c:if test="${empty path}">
        <spring:url value="/playQuizzo" var="${path}"/>
    </c:if>

    <c:if test="${empty label}">
        <spring:message var="label" code="${labelCode}"/>
    </c:if>

    <util:panel id="${id}" title="${label}" openPane="${openPane}">
        <form:form method="POST" action="${path}"
                   modelAttribute="${modelAttribute}"
                   enctype="${enctype}">
            <form:errors cssClass="errors" delimiter="&lt;p/&gt;"/>
            <jsp:doBody/>
        </form:form>

    </util:panel>
</jsp:root>

I have the when and otherwise in there to handle the case where we're submitting back to ourselves (web flow) and otherwise, if a path was given, submit to that path.

Anyhoo... the button group

Ok, so I also made a buttons tag to group buttons together, because on the Roo forms the singluar button was wrapped in a styled div:

<jsp:root ...>
  <jsp:output omit-xml-declaration="yes"/>
  <div class="submit" id="form_submit">
      <jsp:doBody />
  </div>
</jsp:root>

Really just a wrapper div, you know...

The button tag

Then, I put together a button tag which configures buttons for the form. You can add as many as you want. This is in tags/form/field/button.tagx:

<jsp:root 
   xmlns:c="http://java.sun.com/jsp/jstl/core" 
   xmlns:fn="http://java.sun.com/jsp/jstl/functions"
   xmlns:spring="http://www.springframework.org/tags" 
   xmlns:form="http://www.springframework.org/tags/form"
   xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0">
    <jsp:output omit-xml-declaration="yes"/>

   

    <c:if test="${empty text}">
        <spring:message code="label_${fn:toLowerCase(fn:substringAfter(id,'_'))}" var="label" htmlEscape="false"/>
    </c:if>

    <c:set var="sec_field">
        <spring:escapeBody javaScriptEscape="true">${field}</spring:escapeBody>
    </c:set>

    <c:if test="${empty validate or validate}">
        
    </c:if>

    <input id="_${sec_id}_id" type="submit" value="${text}" name="${value}"/>
</jsp:root>

The gist of this tag is to create the same validator declaration on the buttons (as long as they need validation) as before. Note that I don't turn my buttons into Dojo buttons. Neither does the Roo team. I guess they felt that the standard submit buttons were OK. My goal here is to make the library conform, so any stylesheet manipulation I do will work equally well for scaffolded forms as well as my new ones.

Using the forms library, or where I got cranky

Ok, you see now my goal was to make life easy for web flow and non-scaffolded forms developers. Also I wanted to extend what we already had - not dip into the Spring MVC form tag libraries for fields, but use the Roo ones.

And that's where the fun stopped for a bit.

Label conventions

Let me lay this out, as I've tried to explain in Roo in Action but have more space here. The conventions that I am aware of for forms, fields, and other things are not well understood even by me. For exmample, if I plop a <fields:input%gt; tag down, and want to set the label, I have to remember to do this:

  • Set the label up in messages.properties
  • Prefix the message id itself in the properties file with label_
  • Use the label without the label_ prefix in the JSPX page
  • Use the labelCode attribute rather than label
  • Most cryptically: use _field_id as the id of the form fields, as opposed to the field id, as the tag will write an HTML tag with the un-underscored field name and id, and a Dojo dijit for the field with the underscored name. Otherwise, your field will not be submitted

I logged a few JIRA issues as suggestions to implement this tag set and also to make overall non-scaffolded ui more kind to the developer. We'll see how this evolves over time.

Using the tag library:

Ok, so that means I have something like this in my JSPX file:

  <form:form
          id="f_quizzo_flow_register_team"
          labelCode="webflow_playQuizzo_register_team_title"
          modelAttribute="teamSetupForm">
      <fields:input id="_name" field="name"
              labelCode="webflow_playquizzo_register_team_name_field_label"
              required="true" min="1" max="80" />
      <fields:buttons>
          <fields:button id="continue" text="Continue" 
                      value="_eventId_continue"/>
      </fields:buttons>
  </form:form>

and this in my properties file:

label_webflow_playquizzo_register_team_name_field_label=Team Name

That took me a bit of time to unwind as I was writing my form, but in the end, it made it more concise. And I forced myself to locallize. Well, almost completely.

Next up, I complete the web flow and get the state machine going.

More later...

Monday
Feb282011

Adding deeper logging to Webflow Exceptions

If you've been using Roo and the JSPX views with WebFlow, you've probably noticed that you don't get the embedded stack trace in the errors that result.  You can fix this with a lifecycle listener:

First, define the class:

package mypackage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.FlowExecutionListenerAdapter;
import org.springframework.webflow.execution.RequestContext;

public class LoggingFlowExecutionListener extends FlowExecutionListenerAdapter {
  Logger logger = LoggerFactory.getLogger(getClass());
  @Override
  public void exceptionThrown(RequestContext context,
    FlowExecutionException exception) {	
    super.exceptionThrown(context, exception);
    logger.error("Webflow " + context.getActiveFlow().getId() + 
          " threw exception in " + context.getCurrentState().getId(), exception);
    }
}

Then, in webflow-config.xml, replace

  &lt;webflow:flow-executor id="flowExecutor" /&gt;

with

&lt;webflow:flow-executor id="flowExecutor"&gt;
   &lt;webflow:flow-execution-listeners&gt;
     &lt;webflow:listener ref="loggingListener"/&gt;
   &lt;/webflow:flow-execution-listeners&gt;
&lt;/webflow:flow-executor&gt;

&lt;bean class="mypackage.LoggingFlowExecutionListener" id="loggingListener" /&gt;

 

Now you can enjoy useful logging from your Webflow exceptions.

Sunday
Oct032010

Webflow + Roo Again - a More Complex Example...

Anyone want more info on Roo and Webflow?

Sure, you all do...

Before I start, I have to tell a quick story. I wrote this example because I didn't see good samples for the newer convention-driven webflow service call syntax. I also wanted to see what the bare-bones webflow JSPX pages would look like with the new tags.

I also needed a good example for chapter 6 of Roo in Action, which covers WebFlow, GWT and Flex. To make sure I was getting everything right, I put together this work in progress.

After I got done, I did a Google search to see whether or not anybody else had done samples like mine, which is a simple (not complete) shopping cart. It turns out that Willie Wheeler wrote a very good shopping cart example back in 2008. I encourage you to read up on that one, which is much more comprehensive than mine.

However, this is a Roo + WebFlow example, so I think the example is still quite valid.

The Example - a Shopping Cart

Yeah, yeah, yeah, shopping carts. Everybody has them, and mine is more lame! But since I'm more concerned with WebFlow mechanics than use-case perfection, let's just accept that mine is a very rudimentary example.

We're just going to hard-code three products, and allow users to add a quantity of each to a fictional cart. We will also allow users to remove the elements from the cart as well. Later phases of the webflow remain uncoded; this is just a getting started guide for now.

Installing WebFlow with Roo

This is the easiest part - just open the Roo shell on an existing project, and type

web flow

This will install all support for Web Flow, and drop a sample flow in the WEB-INF/views/sampleflow directory. You can check that out to get a feel for the mechanics, but here are the basics:

  • Web Flows are XML-driven state machines. Each user interacts with a web flow in a miniature session, called the Flow Context.
  • Flows are comprised of states and transitions.
  • Flows can store information between requests in various scopes, including the Flow Scope, which exists until the user exits the flow by hitting an end state.
  • Web Flows can execute business logic within a number of places
  • Any Spring Bean in the Application Context is available by id
  • If you build a Spring Bean that extends the WebFlow MultiAction class, and name your methods in a specific way, you can refer to them without passing a full signature.
  • To trigger a transition, you submit back to the web flow, passing it a special form variable (as shown in the examples below) with the name of the transition.

The Shopping Cart flow

We start with the flow definition preamble:

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

  <persistence-context />

We are using the persistence-context tag to enable JPA persistence tracking. Our JPA object, ShoppingCart, is held in this persistence context, and is our form object in the various views. You should note that if you want to use entities in your context, you should make them serializable.

Now, we start the flow. The first stage:

<var name="shoppingCart" class="com.chotchkies.model.ShoppingCart" />

<!-- A sample view state -->
<view-state id="show-cart">
  <transition on="add" to="show-products"/>
  <transition on="checkout" to="end-state"/>
  <transition on="removeItem" to="remove-item">
    <set name="flowScope.productId" value="requestParameters.productId" 
       type="java.lang.Long" />
  </transition>
  <transition on="empty">
    <evaluate expression="cartManager.clearCart" />
  </transition>
</view-state>

The first thing the flow does is pre-create our root JPA entity, ShoppingCart. This is stored within a special holder called the flow scope. Since it is a JPA entity, it is automatically flushed and persisted on each transition.

We then render our first view-state, show-cart. WebFlow tries to resolve the view name by looking up the definition in the Tiles view.xml file, located in the flow directory. I've replaced a more complex file with wildcard support, which was recently added in the newest Tiles release:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN" "http://tiles.apache.org/dtds/tiles-config_2_1.dtd">
<tiles-definitions>
    <definition extends="default" name="*">
        <put-attribute name="body" value="/WEB-INF/views/cart/{1}.jspx"/>
    </definition>
</tiles-definitions>

Note - this makes it possible to just drop new files in the flow definition directory, WEB-INF/views/cart, without modifying the views.xml file each time.

looks for a file named show-cart.jspx, within

We have several transitions, or exit paths, from this view-state:

  • add - transition to the show-products view-state
  • checkout - transition to the end-state
  • removeItem - transition to the remove-item state, but first pull the submitted productId and store it in the flowScope as productId
  • empty - a transition without a destination - this executes a method in the object named cartManager called clearCart - which removes the products from the user's shopping cart.

How do we execute these transitions? Here is a snippet from the button bar at the bottom of the cart page:

<form:form>
  <input type="submit" id="add" name="_eventId_add" value="Add Additional Items..." />
  <input type="submit" id="checkout" name="_eventId_checkout" value="Checkout" />
  <input type="submit" id="empty" name="_eventId_empty" value="Empty Cart" />
</form:form>

You can see the special names - they reference the transition name after the prefix _eventId and an additional underscore. When Web Flow sees these tags, it attempts to perform the transition attached to that tag.

More About Expressions and Convention

When navigating to the show-products state, an on-entry event is triggered, which fires off a call to a Spring Bean, CartManager. Here is the fragment:

<on-entry>
  <evaluate expression="cartManager.getAllProductsNotInCart" />
</on-entry>

The CartManagerImpl class, which extends the WebFlow MultiAction base class, has this method signature for getAllProductsNotInCart:

public Event getAllProductsNotInCart(RequestContext context)

Because it uses this syntax, we don't need to reference the parameters in the XML definition. Nice touch, eh? This pushes some of the details into the bean itself, but also can simplify the XML definition. Here is the full method:

@Override
public Event getAllProductsNotInCart(RequestContext context) {
  ShoppingCart cart = getCart(context);
  Set<Long> keySet = cart.getItems().keySet();
  List<Product> products = Product.findProductsNotIn(keySet);
  context.getViewScope().asMap().put("productList", products);
  return success();
}

private ShoppingCart getCart(RequestContext context) {
  ShoppingCart cart = (ShoppingCart) context.getFlowScope().get("shoppingCart");
  return cart;
}

This shows a few helpful conventions. First, the RequestContext is a class that provides access to all of the scopes, including flowScope and viewScope. In this case, because each time we show the products we want to re-evaluate whether they are in the cart, we place the information in the viewScope variable. This data is only held while rendering the view.

Our helper method, getCart, shows how to access the flow scope. Remember the shoppingCart variable defined in the var tag at the top of the flow? Yep, it's accessed using the context.getFlowScope() method.

Wrap-up - the full flow example

Those are some of the key conventions used by Web Flow. I'm working on this sample, which I'll be adding to a GIT repository soon. For now, here is the rest of the flow, and a complete sample page.

Cart Flow

Notice the end-state - it has a special attribute, commit=true. This makes sure that on exit of the end state, any in-flight JPA changes will be flushed and committed. This is the normal behavior on each step, but when leaving a flow in error, you may wish to have an error end state that does not commit.

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

  <persistence-context />

  <var name="shoppingCart" class="com.chotchkies.model.ShoppingCart" />

    <!-- A sample view state -->
    <view-state id="show-cart">
      <transition on="add" to="show-products"/>
      <transition on="checkout" to="end-state"/>
      <transition on="removeItem" to="remove-item">
        <set name="flowScope.productId" value="requestParameters.productId" 
           type="java.lang.Long" />
      </transition>
      <transition on="empty">
        <evaluate expression="cartManager.clearCart" />
      </transition>
    </view-state>

    <action-state id="remove-item">     
      <evaluate expression="cartManager.removeItemFromCart" />
      <transition on="success" to="show-cart" />
    </action-state>

    <view-state id="show-products">
      <on-entry>
        <evaluate expression="cartManager.getAllProductsNotInCart" />
      </on-entry>
      <transition on="select" to="confirm-product">               
        <evaluate expression="cartManager.configureCartItem" />
      </transition>
      <transition on="cancel" to="show-cart" />
    </view-state>

    <view-state id="confirm-product" model="flowScope.currentItem">           
      <transition on="confirm" to="show-cart"/>             
      <transition on="cancel" to="show-cart">
        <evaluate expression="cartManager.removeItemFromCart" />
      </transition>
    </view-state>

  <action-state id="add-product">
    <!-- KJR - todo -->
    <transition to="show-cart"/>
  </action-state>

  <view-state id="enter-address">
    <transition on="continue" to="confirm-shipping"/>
    <transition on="back" to="checkout-cart"/>
  </view-state>

  <view-state id="confirm-shipping">
    <transition on="continue" to="end-state"/>
    <transition on="back" to="enter-address"/>
  </view-state>

  <end-state id="end-state" view="end-state" commit="true"/>

</flow>

The show-products.jspx view

Note the use of the special variable, ${flowExecutionKey}, which represents the proper webflow flow for the server. If you're writing your own links, and not posting the form, you need to include this. Also note that all scoped variables are 'just there' in the context by their names.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page" 
  xmlns:page="urn:jsptagdir:/WEB-INF/tags/form" 
  xmlns:table="urn:jsptagdir:/WEB-INF/tags/form/fields" 
  xmlns:c="http://java.sun.com/jsp/jstl/core" 
  xmlns:fn="http://java.sun.com/jsp/jstl/functions" 
  xmlns:util="urn:jsptagdir:/WEB-INF/tags/util" 
  xmlns:form="http://www.springframework.org/tags/form" 
  version="2.0">
    <jsp:output omit-xml-declaration="yes"/>
    <h3>Shopping Cart Contents</h3>

    <ul>
      <c:forEach items="${productList}" var="product">
        <li>${product.name} - Price: ${product.price} -
         <c:url var="addUrl" value="">
          <c:param name="productId" value="${product.id}"/>
          <c:param name="_eventId" value="select"/>
          <c:param name="execution" value="${flowExecutionKey}" />
        </c:url>
          <a href="${addUrl}">Add...</a>
        </li>      
      </c:forEach>
    </ul> 

    <form:form>
      <input type="submit" id="cancel" name="_eventId_cancel" value="Cancel and return to cart..." />
    </form:form>

</div>
Friday
Apr112008

Grails and Spring WebFlow

Everyone has had to code an application at some point where they were forced into a particular set of navigational flows. There are a few ui-centric workflow packages out there, including Open Symphony's OSWorkflow, and Spring's WebFlow. Other developers at my shop have worked with WebFlow and were pleased with its' features. But did you know that Grails embeds WebFlow and makes it available within its' controllers automatically?

WebFlow Basics

For a thorough introduction to Spring Webflow, I suggest visiting the SpringSource site. However, in general, webflows can be broken up into distinct components:


  • State - This is a 'definable moment' within the webflow, such as a View State, where the application is waiting on user input.

  • Transition - An event, often fired by a user taking an action, that moves the webflow from one state to another.
  • Action - Code that can be performed within a transition, or on the start or ending of a given State

  • View - A (GSP) page that is rendered during a 'view state'.
  • Flow Scope - A semi-session-like container that lives for the life of the webflow. Data captured from one view to the next

Without too much more ceremony, I can simply say that although Spring WebFlows are a great feature, and take a lot of pain out of implementing flow-based application logic, they are made even easier by Grails.

A Grails WebFlow

Here is a simple webflow that represents a fragment of a voting application. Call it KrimpleVote (or HangingChad, the system)... (I have since lost the image itself...) To build a webflow in Grails, simply create a closure ending in Flow, and embed the states, transitions and actions as inner closures. It's easier to read than it is to describe:



def registrationFlow = {

showDisclaimer {
on("continue").to "lookupVoter"
}

lookupVoter {
on("selfRegister").to "selfRegister"
on("find") {
if (!params.ssn) {
no()
return error()
}

def voter = Voter.findBySsn(params.ssn)
if (voter == null || voter.ssn == null) {
no()
return error()
} else {
flow.voter = voter
}
}.to "showRegistration"
on("return").to "lookupVoter"
}

selfRegister {
on("continue") {
Voter v = new Voter(params)
if (!v.validate()) return error()
v.save()
flow.voter = v
}.to "chooseParty"
}
...

View states are closures that do not automatically transition to another state. Take a look at the showDisclaimer closure as it's a view state. It waits for the 'continue' event to be sent as a transition from the user. You do that by submitting a form to the same closure (registration) and embedding a submitButton within the form with the name of the transition:








fragment of showDisclaimer.gsp

Note: You have to put the GSPs in a subdirectory named registration under the controller's views as it puts all webflow views in a subdirectory by flow name (without Flow). Also, the pages are pretty straightforward as well. Another nice thing is that it automatically handles the flow continuation key for you.

I've been (slowly) getting around to putting that Voter application together as a sample. Once I do, I will post it for download. Until then, I'll post some snippets as I learn more.

For more Flow goodness, check out the grails documentation page on it.