5 minutes with – Spring WebFlow

Spring Framework makes available a powerful package named WebFlow that allows the building of step-by-step web navigation.

It’s common to have to deal with check out shopping cart in e-commerce web site or, completely different process as retrieve password.

In this article I’d like to introduce this technology making an airline booking process example.

Process as check out shopping cart is, commonly, based on a step-by-step navigation where the user has to insert the data in a correct order based on the step of the process.

Spring WebFlow is made to achieve this goal by using a processor descriptor composed by blocks.
View state block identifies a process state (step) associated with a view (user interface) and the moving from one to another is called transition.

Others state types are Action and Decision states. They are both used to make decisions (if-then-else paradigm).
These two types are very similar, and I found out a discussion about the use of the first instead of the second in this thread (http://stackoverflow.com/questions/11994847/spring-webflow-decision-state-vs-action-state).

I talked about an airline booking process at the begin of this post.

It looks something like this:

webflow1

Briefly, The steps are:

  • Choose the flight route;
  • Choose the flight time;
  • Insert the passengers details;
  • Check whether there’s any frequent traveller included;
  • Review the booking confirmation.

Don’t spend too much time to understand the flow, it’s not the core of the post.

Let’s move on to the process descriptor file:

<on-start>
<evaluate expression="bookingFactory.createBooking()" result="flowScope.booking" />
</on-start>

<view-state id="routelist" model="booking">
<transition to="timetable" on="next">
<evaluate expression="bookingValidator.validateRouteFlight(flowScope.booking, messageContext)" />
<evaluate expression="bookingFactory.getRouteTimetable(flowScope.booking)" result="flowScope.timetable" />
</transition>
</view-state>

<view-state id="timetable" model="booking">
<transition to="passengerdetail" on="next">
<evaluate expression="bookingValidator.validateTimeTableFlight(flowScope.booking, messageContext)" />
</transition>
<transition to="routelist" on="previous"></transition>
</view-state>

<view-state id="passengerdetail" model="passenger">
<on-render>
<evaluate expression="bookingFactory.createPassenger()" result="viewScope.passenger" />
<evaluate expression="bookingFactory.getNumberOfPassenger(flowScope.booking)" result="viewScope.numberofpassenger" />
<evaluate expression="bookingFactory.getSeatAlreadyBooked(flowScope.booking)" result="viewScope.seatalreadybooked" />
</on-render>

<transition to="passengerdetail" on="addpassenger">
<evaluate expression="bookingValidator.validatePassenger(viewScope.passenger, messageContext)" />
<evaluate expression="bookingFactory.addPassenger(flowScope.booking,viewScope.passenger)" />
</transition>
<transition to="frequenttraveller" on="next">
<evaluate expression="bookingValidator.validatePassengerNumber(flowScope.booking, messageContext)" />
</transition>
<transition to="timetable" on="previous"></transition>
</view-state>
<decision-state id="frequenttraveller">
<if test="bookingFactory.isFrequentTraveller(flowScope.booking)" then="congratfrequenttraveller" else="reviewbooking" />
</decision-state>
<view-state id="reviewbooking">
<transition to="bookingconfirmed" on="confirm"></transition>
<transition to="passengerdetail" on="previous"></transition>
</view-state>

<view-state id="congratfrequenttraveller">
<on-render>
<evaluate expression="bookingFactory.getFrequentTraveller(flowScope.booking)" result="viewScope.frequentTraveller" />
</on-render>
<transition to="reviewbooking" on="confirm"></transition>
</view-state>

<end-state id="bookingconfirmed" view="bookingconfirmed"></end-state>

This file uses some component as BookingFactory and BookingValidator.
While the first contains the Business Logic of the process, the last includes the method to validate the user input.

@Component
public class BookingFactory {

public Booking createBooking()
{
Booking booking = new Booking();
Flight flight = new Flight();
booking.setFlight(flight);

return booking;
}

public Flight createFlight()
{
Flight flight = new Flight();
return flight;
}

public Passenger createPassenger()
{
Passenger passenger = new Passenger();
return passenger;
}

public List<String> getRouteTimetable(Booking booking)
{
return TimeTable.timetable.get(booking.getFlight().getFlightRoute());
}

public void addPassenger(Booking booking, Passenger passenger)
{
List<Passenger> passengers = booking.getPassenger();
if (passengers==null)
passengers = new ArrayList<Passenger>();

passengers.add(passenger);
booking.setPassenger(passengers);
}

public int getNumberOfPassenger(Booking booking)
{
if (booking.getPassenger()==null)
return 0;
else
return booking.getPassenger().size();
}

public List<String> getSeatAlreadyBooked(Booking booking)
{
List<String> seats = new ArrayList<String>();
if (booking.getPassenger()!=null)
{
for (Passenger passenger : booking.getPassenger())
seats.add(passenger.getSeatNumber());
}

return seats;
}

public boolean isFrequentTraveller(Booking booking)
{
for (Passenger passenger : booking.getPassenger())
{
for (Passenger frequentTraveller : FrequentTraveller.frequentTraveller)
{
if (frequentTraveller.equals(passenger))
return true;
}
}

return false;
}

public Passenger getFrequentTraveller(Booking booking)
{
for (Passenger passenger : booking.getPassenger())
{
for (Passenger frequentTraveller : FrequentTraveller.frequentTraveller)
{
if (frequentTraveller.equals(passenger))
return frequentTraveller;
}
}

return null;
}
}

And BookingValidator

@Component
public class BookingValidator {

public Event validateRouteFlight(Booking booking, MessageContext messageContext){
if(booking.getFlight().getFlightRoute() == null 
|| booking.getFlight().getFlightRoute().trim() == ""){
MessageBuilder errorMessageBuilder = new MessageBuilder().error();
errorMessageBuilder.source("flight.flightRoute");
errorMessageBuilder.code("flightRouteRequired");
messageContext.addMessage(errorMessageBuilder.build());
return new EventFactorySupport().error(this);
}
return new EventFactorySupport().success(this);
}

public Event validateTimeTableFlight(Booking booking, MessageContext messageContext){
if(booking.getFlight().getTimeDeparture() == null 
|| booking.getFlight().getTimeDeparture().trim() == ""){
MessageBuilder errorMessageBuilder = new MessageBuilder().error();
errorMessageBuilder.source("flight.timeDeparture");
errorMessageBuilder.code("flightTimeDepartureRequired");
messageContext.addMessage(errorMessageBuilder.build());
return new EventFactorySupport().error(this);
}
return new EventFactorySupport().success(this);
}

public Event validatePassenger(Passenger passenger, MessageContext messageContext){
if(passenger.getFirstName() == null || passenger.getFirstName().trim() == "" ||
passenger.getLastName() == null || passenger.getLastName().trim() == "")&nbsp; {

MessageBuilder errorMessageBuilder = new MessageBuilder().error();
errorMessageBuilder.source("firstName");
errorMessageBuilder.code("passengerNameRequired");
messageContext.addMessage(errorMessageBuilder.build());

return new EventFactorySupport().error(this);
}
return new EventFactorySupport().success(this);
}

public Event validatePassengerNumber(Booking booking, MessageContext messageContext){

if(booking.getPassenger() == null || booking.getPassenger().size() <= 0)&nbsp; {

MessageBuilder errorMessageBuilder = new MessageBuilder().error();
errorMessageBuilder.source("firstName");
errorMessageBuilder.code("passengerNumberRequired");
messageContext.addMessage(errorMessageBuilder.build());

return new EventFactorySupport().error(this);
}
return new EventFactorySupport().success(this);
}
}

Let’s see the first step, the route list.

webflow2

Now, only one transaction is available in this step (next transaction).

Let me go deeper in the code that build this block;

<view-state id="routelist" model="booking">
<transition to="timetable" on="next">
<evaluate expression="bookingValidator.validateRouteFlight(flowScope.booking, messageContext)" />
<evaluate expression="bookingFactory.getRouteTimetable(flowScope.booking)" result="flowScope.timetable" />
</transition>
</view-state>

At the first row is described the state id (routelist) that’s also used as the jsp page’s name. Model attribute is the class name of the bean used in Jps page.

After that, the transaction to “timetable” includes to expression; the first is validation form and the second is used to prepare the next step.

The Jsp page is the following:

<form:form modelAttribute="booking" action="${flowExecutionUrl}">
<h1 style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 32px;">Routing Map</h1>
<div style="height:4px;"></div>
<div style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 10px; height:100px;">
<form:errors path="flight.flightRoute" cssClass="error"/>
<form:radiobutton path="flight.flightRoute" value="LGW-MXP" />London Gatwick - Milan Malpensa
<form:radiobutton path="flight.flightRoute" value="MAD-FRA" />Madrid - Frankfurt International
<form:radiobutton path="flight.flightRoute" value="CDG-LIS" />Paris Charles de Gaulle - Lisbon
</div>
<div style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 10px;">
<table width=100%">
<tr>
<td align="left">
<table width="40%"&nbsp; border="0" cellspacing="3" cellpadding="1">
<tr>
<td align="left">
<table width="30%"&nbsp; border="0" cellspacing="3" cellpadding="1">
<tr>
<td><button type="submit" id="next" name="_eventId_next">
<spring:message code="next"/>
</button></td>
</tr>
</table>
</td></tr>
</table>
</td></tr>
</table>
</div>
</form:form>

A special mention is for the first line where we declare the same Model attribute (booking).

The second step is the departure time.

webflow3

This step includes two possible transactions: the “next” for choosing the seat and the passenger and the “previous” for backing at the route list.

The third step is dedicated at the passenger’s name and seat place. This step provides the feature to add more than only one passenger.

webflow4

The “addpassenger” transaction takes the user to the same page adding the passenger to the passenger’s list.
If one passenger is included in the Frequent Travel list, the user will be move to a congratulation page.

webflow5

In order to make this check, the decision-state is called by the flow and the bookingFactory.isFrequentTraveller executed.

The last step is the booking review:

webflow6

As you can guess, this page summarize the booking information.

The source code is available on gitHub at https://github.com/MarcoGhise/SpringWebFlowFlightBooking.

My final thought is about this technology. In my opinion this is not a Spring version of “Struts” that helps the user to build an entire website flow.

Spring WebFlow should be used in very specific flow process, only in this way it can be really be useful.

Thank Raghuveer Bhandarkar for sharing his post that I’ve used as inspiration of my example.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s