CHAPTER 9

Creating Web Flows

The majority of modern web frameworks are heavily optimized for dealing with stateless interaction between client and server. In other words, these frameworks assume that you'll be defining simple actions that remember nothing about where the user came from, where the user is going, and what state the user is in. The stateless model offers you advantages, of course, including the ability to scale your application's hardware without the need to consider replication of state.

Nevertheless, you'll occasionally face significant disadvantages to implementing certain use cases with a stateless model. Take, for instance, the good old shopping-cart use case. A shopping cart typically entails a step-by-step process of accepting and displaying information before progressing to the next screen. A shopping-cart user might go through these steps:

  • Confirm the basket items and their prices.
  • Enter a shipping address.
  • Enter a billing address.
  • Enter credit-card details.
  • Confirm the order.
  • Show the invoice.

You don't want one of your users jumping into the middle of this inherently sequential process. Of course, this restriction applies not only to shopping carts, but also to any functionality that is "wizard-like" in nature. To implement a shopping-cart use case with a stateless model, you would need to store the state somewhere—maybe by storing it in the session or using cookies. You would then need code in your actions to ensure users arrive from a point that's appropriate. In other words, you don't want the user going straight from confirming the basket items to showing the invoice. The user needs to have originated from the correct place.

However you look at it, implementing this use case in a stateless model is a pain. One way to alleviate this pain would be to use Asynchronous JavaScript Technology and XML (Ajax) to manage all the state on the client, which we discussed in the previous chapter. You could reasonably push all this state management to the client by eliminating the need to refresh the browser. The individual steps in your flow could be HTML elements that are shown or hidden depending on your current state.

However, Ajax is not an option for everyone just yet, and certainly not the solution to every problem. Occasionally, it makes a great deal of sense to push this job onto the server.

Fortunately, Grails provides built-in support to aid the creation of rich web flows, often referred to as "web conversations." Built on the excellent Spring Web Flow project (http://www.springframework.org/webflow), flows are integrated seamlessly into Grails with a Groovy domain-specific language (DSL) for flow creation.

Getting Started with Flows

Flows in Grails are a mechanism for defining a series of states, beginning with a start state and terminating with an end state. With Grails flows, it is impossible for users to call your server in the "middle" of a flow unless they have gone through the necessary steps (or states) to reach that point.

How does it work? Spring Web Flow is, essentially, an advanced state machine. A flowExecutionKey and event ID is passed between client and server, typically as a request parameter, which allows a user to transition from one state to another. Don't get too worked up about the mechanics of this; Grails deals with most of the communication between client and server for you. What you do need to know, however, is how to define a flow.

Defining a Flow

Unlike the Spring Web Flow project itself, Grails doesn't require any XML configuration to get going. To create a flow, simply define an action in your controller whose name ends with the "Flow" suffix (by convention). Listing 9-1 shows how you would define a hypothetical shopping-cart flow.

Listing 9-1. Defining a Flow

class StoreController {
    def shoppingCartFlow = {
        ...
    }
}

Every flow has what is known as a "flow id." The flow id, by convention, is the name of the action minus the "Flow" suffix. In Listing 9-1, you defined an action called shoppingCartFlow; hence the flow id is shoppingCart. The importance of the flow id will become more relevant when we look at linking to flows and creating flow views.

Defining the Start State

Currently, the shoppingCart flow doesn't look all that different from a regular controller action. However, the way you construct a flow differs greatly. First of all, unlike actions, the body of the closure doesn't define the logic; instead, it defines a sequence of flow states. States are represented as method calls that take a closure parameter. Listing 9-2 shows how you can define the start state of the shoppingCart flow.

Listing 9-2. Defining the Start State

def shoppingCartFlow = {
    showCart {
        on("checkout").to "enterPersonalDetails"
        on("continueShopping").to "displayCatalogue"
    }
    ...
}

The start state is always the first state in the flow. The start state in Listing 9-2, highlighted in bold, is called showCart. It's a "view" state as well as a start state.

View states pause the flow execution for view rendering, allowing users to interact with the flow. In this case, because the flow id is shoppingCart and the state is called showCart, by convention Grails will look for a GSP at the location grails-app/views/store/shoppingCart/showCart.gsp. In other words, unlike regular actions, which look for their views relative to the controller directory (grails-app/views/store), flow views exist in a subdirectory that matches the flow id—in this case, grails-app/views/store/shoppingCart.

You'll notice in Listing 9-2 that the showCart state has two event handlers. Invoking the on method and passing the name of the expected event defines an event handler. You can then define what the event handler should do in response to the event by calling the to method of the return value. Here is the example:

on("checkout").to "enterPersonalDetails"

This line specifies that when the checkout event is triggered, the flow should transition to the enterPersonalDetails state. Simple, really. In programming terminology, DSLs that let you use a method's return value to chain method calls in this way are often referred to as "fluent APIs". You'll learn more about events and triggering events later in the chapter.

Defining End States

A flow's end state is a state that essentially terminates the flow's execution. Users must start at the beginning of the flow once an end state has been triggered. An end state is either a state that takes no arguments, or one that performs an external redirect to another action or flow. Listing 9-3 shows how to define a couple of end states for the shoppingCart flow, called displayInvoice and cancelTransaction.

Listing 9-3. Defining an End State

def shoppingCartFlow = {
    showCart {
        on("checkout").to "enterPersonalDetails"
        on("continueShopping").to "displayCatalogue"
    }
         ...
    displayInvoice()
    cancelTransaction {
        redirect(controller:"store")
    }
}

While the displayInvoice end state renders a view called grails-app/views/store/shoppingCart/displayInvoice.gsp, the cancelTransaction end state performs a redirect to another controller.

Action States and View States

Between the start and end states, you'll typically have several other states, which are either the aforementioned view states or action states. Just to recap: A view state pauses the flow execution for view rendering; it doesn't define an action or a redirect. As we mentioned, the start state in Listing 9-2 is also a view state.

By default, the name of the view to render comes from the state name. However, you can change the name of the view to render by using the render method, as you do with regular controller actions. Listing 9-4 demonstrates how to render a view at the location grails-app/views/store/shoppingCart/basket.gsp simply by specifying the name "basket."

Listing 9-4. Changing the View to Render in a View State

showCart {
    render(view:"basket")

    ...

}

An action state differs from a view state in that instead of waiting for user input, it executes a block of code that dictates how the flow should transition. For example, consider the code in Listing 9-5.

Listing 9-5. An Action State

listAlbums {
   action {
      [ albumList:Album.list(max:10,sort:'dateCreated', order:'desc') ]
   }
   on("success").to "showCatalogue"
   on(Exception).to "handleError"
}

The listAlbums state defines an action by calling the action method and passing in the block of code that defines the action as a closure. In this case, the action obtains a list of the 10 newest albums and places the list items into a Map with the key albumList. This map is returned as the model for the action and is automatically put into flow scope. Don't be too concerned about this statement; flow scopes are the subject of the next section.

As well as demonstrating how to supply a model from a flow action, the code in Listing 9-5 introduces a couple of other new concepts. First of all, if no error occurs when the flow action is executed, the success event is automatically triggered. This will result in the flow transitioning to the showCatalogue action.

Finally, the code contains a second event handler that uses a convention we haven't seen yet. By passing in the exception type to the on method, you can specify event handlers for particular types of exception. Listing 9-5 includes a generic exception handler that catches all subclasses of java.lang.Exception, but you could just as easily catch a more specific exception:

on(StoreNotAvailableException).to "maintenancePage"

In the example in Listing 9-5, the action defines and returns a model. However, action states can also trigger custom events from the action. For example, take a look at Listing 9-6.

Listing 9-6. Triggering Events from an Action State

isGift {
    action {
        params.isGift ? yes() : no()
    }
    on("yes").to "wrappingOptions"
    on("no").to "enterShippingAddress"
}

In the example in Listing 9-6, the code defines an action state that inspects the params object to establish whether the user has requested to have her purchase gift-wrapped. If the user has, the code triggers the yes event simply by calling the method yes(). Note that the return value of the action dictates the event to trigger, so in some cases you might need a return statement as shown in Listing 9-7.

Listing 9-7. Triggering Events Using the Return Value

isGift {
    action {
        if(params.isGift)
             return yes()
        else
             return no()
    }
    on("yes").to "wrappingOptions"
    on("no").to "enterShippingAddress"
}

Note In Groovy 1.6, the notation of returning the last expression in a method or closure was extended to cover if/else blocks. The result is that the return statements in Listing 9-7 are no longer necessary.


Flow Scopes

In addition to scopes found in regular actions such as request and session, you can use a few other scopes associated with Grails flows: flash, flow, and conversation. Scopes are essentially just containers, like maps. The main difference is how and when the objects contained within these scopes are cleared. The following list summarizes the behavior of each scope:

  • flash: Stores the object for the current and next request only.
  • flow: Stores objects for the scope of the flow, removing them when the flow reaches an end state.
  • conversation: Stores objects for the scope of the conversation, including the root flow and nested subflows.

As demonstrated in the previous section, models returned from action states are automatically placed into flow scope. The flow scope is typically the most-used scope in flow development because it allows you to store entries for the length of the entire flow, which are then automatically cleaned up when the flow terminates by reaching an end state or expiring.

The conversation scope differs in that it stores entries for the scope of the root flow and all nested subflows. Yes, Grails' Web Flow support includes support for subflows, which we will discuss later in the chapter.

Finally, flash scope behaves similarly to the regular flash scope provided by controller actions. The main difference is that objects placed into flash scope within a flow must implement the java.io.Serializable interface.

Flows, Serialization, and Flow Storage

The end of the previous section touched on an important issue regarding the use of flows. Whenever placing any object and its associations within one of the flow scopes, you must ensure the object in question implements the java.io.Serializable interface.

Why? Quite simply, flows differ from regular scopes such as session and request in that they store their state in a serialized, compressed form on the server. If you prefer a stateless server, you can instead store the state in the client by setting the grails.webflow.flow.storage property to client in grails-app/conf/Config.groovy:

grails.webflow.flow.storage="client"

In this case, Grails' Web Flow support will store the state in the flowExecutionKey that is passed from client to server. Using client storage has two main disadvantages:

  • You can use only HTTP POST requests—via a form submission, for example—to trigger events, because the flowExecutionKey is too large for browsers to include in the URL.
  • This method is inherently unsecure unless delivered over HTTPS because you are sending potentially sensitive data, in serialized form, to and from the server.

If security is not a concern for your application, or you are happy running your flow over HTTPS, then using a client flow-storage mechanism might make sense because it allows your server to remain stateless. You should, however, make this decision up front because your storage mechanism affects how you implement the flow. (As we mentioned, choosing client storage means you must use POST requests only.)

Whichever choice you make, the requirement to implement java.io.Serializable remains the same. As you will recall from your Java experience, if you have any properties that you don't want serialized in your objects, you must mark them as transient. This includes any closures you might have defined (such as GORM events, which we'll discuss in Chapter 10), because Groovy's closures do not implement the Serializable interface:

transient onLoad = {}

Triggering Events from the View

In the previous section on action states and view states, you learned that a view state is a state that pauses flow execution in order to render a view that takes user input. However, how exactly does the view trigger a flow-execution event? Essentially, there are two ways to trigger an event: from a link, or from a form submission.

Let's look at links first. As we discussed in Chapter 4, Grails provides the built-in <g:link> tag for producing HTML anchor tags that link to a particular controller and action. Linking to flows is pretty much the same as linking to a regular action. For example, the following usage of the <g:link> tag will link to the start state of the shoppingCart flow:

<g:link controller="store" action="shoppingCart">My Cart</g:link>

Note that you use the flow id, discussed earlier, as the value of the action attribute. Of course, linking to the start state allows you to trigger a new flow execution, but doesn't answer the original question of how to trigger flow-execution events. Let's revisit the code from the start state of the shoppingCart flow:

showCart {
    on("checkout").to "enterPersonalDetails"
    on("continueShopping").to "displayCatalogue"
}

In the showCart state, there are two potential events that could be triggered: checkout and continueShopping. To trigger one of these events from the <g:link> tag, you can use the event attribute:

<g:link controller="store" action="shoppingCart" event="checkout">Checkout</g:link>

Note that the value of the action attribute always remains the same when linking to a flow. What differs is the event, and in this case you are telling Grails to trigger the checkout event when the link is clicked.

The mechanism to trigger events from forms is slightly different. Essentially, Grails uses the name attribute of the submit button that was clicked to figure out which event you want to trigger. Using the view state from Listing 9-7, you can easily trigger each event using the <g:submitButton> tag as shown in Listing 9-8.

Listing 9-8. Triggering Events from Form Submissions

<g:form name="shoppingForm" url="[controller:'store', action:'shoppingCart']">
    ...
    <g:submitButton name="checkout" value="Checkout" />
    <g:submitButton name="continueShopping" value="Continue Shopping" />
</g:form>

Transition Actions and Form Validation

In the previous section, we looked at how you can trigger events on form submission. However, we didn't discuss how to validate a form submission. One way is to submit to an action state. In the section on action and view states, we looked at how you can have action states that execute a particular block of code, which is extremely useful for making the state decision dynamic. However, you're better off using a transition action to perform validation.

A transition action is essentially an action that executes when a particular event is triggered. Here's the interesting part: if the transition action fails due to an error, the transition is halted and the state is restored back to the originating state. Take, for example, the state in Listing 9-9.

Listing 9-9. Using Transition Actions for Validation

enterPersonalDetails {
  on("submit") {
        flow.person = new Person(params)
        flow.person.validate() ? success() : error()
  }.to "enterShipping"
  on("return").to "showCart"
}

In the example in Listing 9-9, there is a view state called enterPersonalDetails that renders a form where users can enter their personal information. When the user submits the form, the submit event is triggered. Notice how the on method that defines the submit event is passed a closure. This closure is the submit transition action. Contained within the body of the transition action is code, which creates a new Person domain class and populates the class's properties by passing the params object into the constructor of the Person class.

Notice how the submit transition action then performs validation by calling validate() on the Person instance within the flow. Using the ternary operator, the transition action will return either a success event or an error event. If the result is a success event, the transition to the enterShipping state continues as expected. But if the result is an error event, the transition is halted and the user is returned to the enterPersonalDetails state, where the view can render the errors contained within the person object.

Subflows and Conversation Scope

As we mentioned briefly in the previous section on flow scopes, Grails' Web Flow implementation supports the notion of subflows, or flows within flows. Consider the chooseGiftWrapFlow flow in Listing 9-10, which allows users to select their ideal gift-wrap for a given purchase.

Listing 9-10. The chooseGiftWrapFlow Flow

def chooseGiftWrapFlow = {
    chooseWrapping {
        on("next").to 'chooseRibbon'
        on('cancel').to 'cancelGiftWrap'
    }
    chooseRibbon {
        on("next").to 'confirmSelection'
        on("back").to 'chooseWrapping'
    }
    confirmSelection {
        on('confirm') {
            def giftWrap = new GiftWrap(params)
            if(!giftWrap.validate()) return error()
            else {
                conversation.giftWrap = giftWrap
            }
        }.to 'giftWrapChosen'
        on('cancel').to 'cancelGiftWrap'
    }
    cancelGiftWrap()
    giftWrapChosen()
}

The chooseGiftWrap flow basically goes through three view states (chooseWrapping, chooseRibbon, and confirmSelection) to establish the ideal wrapping for the user. Additionally, there are two end states with pretty self-explanatory names: cancelGiftWrap and giftWrapChosen.

To include the chooseGiftWrap flow in the main shoppingCart flow, you can create a new subflow state by calling the subflow method and passing in a reference to the chooseGiftWrap flow. Listing 9-11 shows an example by defining a wrappingOptions subflow state.

Listing 9-11. Defining a Subflow State

def shoppingCartFlow = {
    ...
    wrappingOptions {
        subflow(chooseGiftWrapFlow)
        on('giftWrapChosen') {
            flow.giftWrap = conversation.giftWrap
        }
        on('cancelGiftWrap'). to 'enterShippingAddress'
    }
}

You should note two critical things about the code in Listing 9-11. First, the wrappingOptions subflow state defines two event handlers called giftWrapChosen and cancelGiftWrap. You will note that these event names match the end states of the chooseGiftWrap flow!

The second important thing is that the confirmSelection state from the chooseGiftWrap flow in Listing 9-10 places an instance of a hypothetical GiftWrap domain class in conversation scope. As we mentioned in the section on flow scopes, conversation scope is shared across all flows and subflows, so it's a good way to pass variables between flows. The giftWrapChosen event handler in Listing 9-11 defines a transition action, which takes the giftWrap variable from conversation scope and places it into local flow scope.

Flows in Action

Now that you know the mechanics of Grails flows, you can put this knowledge into action by developing the next use case for the gTunes application. You have the ability to browse the gTunes Music Library, but gTunes won't be a proper store until users can purchase music!

You'll be selling digital music, so you won't have anything to ship to users. So to make it interesting, you're going to offer a unique, possibly misguided, feature in the gTunes music store: the ability to order a hard-copy CD along with a digital purchase—for free!

To spice things up even further, you're going to implement that classic Amazonesque "recommendations" feature, which tries to tempt users into buying other albums before completing a purchase. Figure 9-1 shows the basic decision-making process that the user follows when stepping through the flow.

image

Figure 9-1. The gTunes purchase flow

Updating the Domain

The first task is to provide prices for the albums in the music store. To do so, open the Album domain class and add a price property. Listing 9-12 shows the changes to the grails-app/domain/Album.groovy file.

Listing 9-12. Adding Prices to the Album Class

package com.g2one.gtunes
class Album implements Serializable{
   ..
   Float price
   static constraints = {
        ...
        price scale:2, nullable:false
   }
}

The code in Listing 9-12 not only adds a price property, but also constrains the price property in two ways. First, the nullable constraint ensures consistency by not allowing the price to be null. Second (and more interesting), the scale constraint is used to ensure that the price is constrained to two decimal places.

With the knowledge that you're developing a flow, you also need to change the Album class to implement the java.io.Serializable interface as required by flows. In fact, because you're likely to use the rest of the domain in the context of the flow, you will need to update all the other existing domain classes to implement Serializable, too.

In addition to these changes to the existing domain, you're going to define three new domain classes to capture various aspects of a transaction. First is a domain class called Payment, which holds the invoice number and a reference to the User who completed the purchase. Listing 9-13 shows the source for the Payment class.

Listing 9-13. The Payment Domain Class

package com.g2one.gtunes
class Payment implements Serializable {
    String invoiceNumber
    User user
    static hasMany = [albumPayments:AlbumPayment]

    static constraints = {
        invoiceNumber blank:false, matches:/INV-d+?-d+/
    }
}

As you can see from the code in Listing 9-13, a Payment has many AlbumPayment instances. The AlbumPayment class is used to track what Albums a User has purchased, as well as the address to which the CD version of the Album needs to be shipped. The source for the AlbumPayment class is shown in Listing 9-14.

Listing 9-14. The AlbumPayment Domain Class

package com.g2one.gtunes

class AlbumPayment implements Serializable{
    Album album
    User user
    Address shippingAddress
    static constraints = {
        shippingAddress nullable:true
    }
}

Finally, to capture an AlbumPayment instance's shipping address, you'll need an Address class. Listing 9-15 shows the Address class with appropriate constraints applied.

Listing 9-15. The Address Domain Class

package com.g2one.gtunes

class Address implements Serializable{
    String number
    String street
    String city
    String state
    String postCode
    String country

    static constraints = {
        number blank:false, maxSize:200
        street blank:false, maxSize:250
        city blank:false, maxSize:200
        state nullable:true
        postCode blank:false, maxSize:50
        country blank:false, maxSize:200
    }
}

Updating the View

With the updates to the domain done, let's move on to the view. Open the grails-app/views/album/_album.gsp template and add the price property to the albumInfo <div>. Now define a new <div> that contains a <g:link> tag that links to a new action of the StoreController called buy. Listing 9-16 shows the changes to the _album.gsp template, highlighted in bold.

Listing 9-16. Updates to the _album.gsp Template

<div id="album${album.id}" class="album" style="display:none;">
   ...
   <div class="albumDetails">
          ...
       <div class="albumInfo">
           Genre: ${album.genre ?: 'Other'}<br>
           Year: ${album.year}<br>
           <strong>Price: $ ${album.price}</strong>
       </div>
       ...
       <div class="albumLinks">
           ...
           <div id="buttons" style="float:right;">
               <g:link controller="store" action="buy" id="${album.id}">
                    <img src="${createLinkTo(dir:'images',file:'buy-button.gif')}"
                                <border="0">
               </g:link>
            </div>
        </div>
    </div>
</div>

The key addition to the _album.gsp template is the link:

<g:link controller="store" action="buy" id="${album.id}">

The <g:link> tag defines a link to the buy action of the StoreController that passes the Album identifier as part of the request. A few CSS tweaks later, you should have something that resembles the screenshot in Figure 9-2.

image

Figure 9-2. Screenshot of the updates _album.gsp template

Defining the Flow

In the previous section, you created a <g:link> tag that referenced an action called buy. As you might have guessed, buy is going to be the name of the flow. Open grails-app/controllers/StoreController and define a new flow called buyFlow, as shown in Listing 9-17.

Listing 9-17. Defining the buyFlow

def buyFlow {
    ...
}

Adding a Start State

Now let's consider the start state. Here's a logical point to start: After a user clicks on the "Buy" button, the application should ask him whether he'd like to receive a CD version of the album. But before you can do that, you should validate whether he is logged in; if he is, you should place him into flow scope.

To achieve this, you can make the first state of the flow an action state. Listing 9-18 shows an action state, called start, that checks if the user exists in the session object and triggers a login() event if not.

Listing 9-18. Checking Login Details with an Action State

1   start {
2       action {
3           // check login status
4          if(session.user) {
5                flow.user = User.get(session.user.id)
6                return success()
7          }
8          login()
9       }
10      on('success') {
11          if(!flow.albumPayments) flow.albumPayments = []
12          def album = Album.get(params.id)
13
14          if(!flow.albumPayments.album.find { it?.id == album.id }) {
15              flow.lastAlbum = new AlbumPayment(album:album)
16              flow.albumPayments << flow.lastAlbum
17           }
18       }.to 'requireHardCopy'
19       on('login') {
20           flash.album = Album.get(params.id)
21           flash.message = "user.not.logged.in"
22       }.to 'requiresLogin'
23   }

The login event handler contains a transition action that places the Album instance into flash scope along with a message code (you'll understand why shortly). The event then causes a transition to a state called requiresLogin, which is the first example of a redirect state. Listing 9-19 shows the requiresLogin state using the objects that were placed into flash scope to perform a redirect back to the display action of the AlbumController.

Listing 9-19. Using a Redirect Action to Exit the Flow

requiresLogin {
    redirect(controller:"album",
                     action:"display",
                     id: flash.album.id,
                     params:[message:flash.message])
}

Hold on a moment; the display action of the AlbumController doesn't return a full HTML page! In the previous chapter, you designed the code to handle Ajax requests and return only partial responses. Luckily, Grails makes it possible to modify this action to deal with both Ajax and regular requests using the xhr property of the request object, which returns true if the request is an Ajax request. Listing 9-20 shows the changes made to the display action in bold.

Listing 9-20. Adapting the display Action to Handle Regular Requests

def display = {
    def album = Album.get(params.id)
    if(album) {
        def artist = album.artist
        if(request.xhr) {
                render(template:"album", model:[artist:artist, album:album])
        }
        else {
                render(view:"show", model:[artist:artist, album:album])
        }
    }
    else {
        response.sendError 404
    }
}

The code highlighted in bold changes the action to render a view called grails-app/views/album/show.gsp if the request is a non-Ajax request. Of course, the shop.gsp view in question doesn't exist yet, and at this point you can consider refactoring some of the view code developed in the previous section. There is a lot of commonality not only for shop.gsp, but also for the pages of the buy flow.

Currently the instant-search box and the top-five-songs panel are hard-coded into the grails-app/views/store/shop.gsp view, so start by extracting those into templates called _searchbox.gsp and _top5panel.gsp, respectively. Listing 9-21 shows the updated shop.gsp view with the extracted code replaced by templates highlighted in bold.

Listing 9-21. Extracting Common GSP Code into Templates

<html>
       ...
       <body id="body">
           <h1>Online Store</h1>
           <p>Browse or search the categories below:</p>
           <g:render template="/store/searchbox" />
          <g:render template="/store/top5panel" model="${pageScope.variables}" />
           <div id="musicPanel">
           </div>
      </body>
</html>

Notice how in Listing 9-21 you can pass the current pages' model to the template using the expression pageScope.variables. With that done, you're going to take advantage of the knowledge you gained about SiteMesh layouts in Chapter 5. Using the magic of SiteMesh, you can make the layout currently embedded into shop.gsp truly reusable. Cut and paste the code within shop.gsp into a new layout called grails-app/views/layouts/storeLayout.gsp, adding the <g:layoutBody /> tag into the "musicPanel" <div>. Listing 9-22 shows the new storeLayout.gsp file.

Listing 9-22. Creating a New storeLayout

<html>
    <head>
        <meta http-equiv="Content-type" content="text/html; charset=utf-8">
        <meta name="layout" content="main">
        <title>gTunes Store</title>
    </head>
    <body id="body">
        <h1>Online Store</h1>
        <p>Browse or search the categories below:</p>
        <g:render template="/store/searchbox" />
        <g:render template="/store/top5panel" model="${pageScope.variables}" />
        <div id="musicPanel">
            <g:layoutBody />
        </div>
    </body>
</html>

Notice how you can still supply the HTML <meta> tag that ensures the main.gsp layout is applied to pages rendered with this layout. In other words, you can use layouts within layouts!

Now that you've cut and pasted the contents of shop.gsp into the storeLayout.gsp file, shop.gsp has effectively been rendered useless. You can fix that using the <g:applyLayout> tag:

<g:applyLayout name="storeLayout" />

With one line of code, you have restored order; shop.gsp is rendering exactly the same content as before. So what have you gained? Remember that when you started this journey, the aim was to create a grails-app/views/album/show.gsp file that the non-Ajax display action can use to render an Album instance. With a defined layout in storeLayout, creating this view is simple (see Listing 9-23).

Listing 9-23. Reusing the storeLayout in show.gsp

<g:applyLayout name="storeLayout">
    <g:if test="${params.message}">
        <div class="message">
            <g:message code="${params.message}"></g:message>
        </div>
    </g:if>
    <g:render template="album" model="[album:album]"></g:render>
</g:applyLayout>

Using the <g:applyLayout> tag again, you can apply the layout to the body of the <g:applyLayout> tag. When you do this in conjunction with rendering the _album.gsp template, it takes little code to render a pretty rich view. We'll be using the storeLayout.gsp repeatedly throughout the creation of the rest of the flow, so stay tuned.

Returning to the start state of the flow from Listing 9-18, you'll notice that the success event executes a transition action. When the transition action is triggered, it first creates an empty list of AlbumPayment instances in flow scope if the list doesn't already exist:

11 if(!flow.albumPayments) flow.albumPayments = []

Then it obtains a reference to the Album the user wants to buy using the id obtained from the params object on line 12:

12 def album = Album.get(params.id)

With the album in hand, the code on line 14 then checks if an AlbumPayment already exists in the list by executing a nifty GPath expression in combination with Groovy's find method:

14 if(!flow.albumPayments.album.find { it?.id == album.id })

This one expression really reflects the power of Groovy. If you recall that the variable flow.albumPayments is actually a java.util.List, how can it possibly have a property called album? Through a bit of magic affectionately known as GPath, Groovy will resolve the expression flow.albumPayments.album to a new List that contains the values of the album property of each element in the albumPayments List.

With this new List in hand, the code then executes the find method and passes it a closure that will be invoked on each element in the List until the closure returns true. The final bit of magic utilized in this expression is the usage of the "Groovy Truth" (http://docs.codehaus.org/display/GROOVY/Groovy+Truth). Essentially, unlike Java where only the boolean type can be used to represent true or false, Groovy defines a whole range of other truths. For example, null resolves to false in an if statement, so if the preceding find method doesn't find anything, null will be returned and the if block will never be entered.

Assuming find does resolve to null, the expression is then negated and the if block is entered on line 15. This brings us to the next snippet of code to consider:

15         flow.lastAlbum = new AlbumPayment(album:album)
16         flow.albumPayments << flow.lastAlbum

This snippet of code creates a new AlbumPayment instance and places it into flow scope using the key lastAlbum. Line 15 then adds the AlbumPayment to the list of albumPayments held in flow scope using the Groovy left shift operator << — a neat shortcut to append an element to the end of a List.

Finally, with the transition action complete, the flow then transitions to a new state called requireHardCopy on line 18:

18 }.to 'requireHardCopy'

Implementing the First View State

So after adding a start state that can deal with users who have not yet logged in, you've finally arrived at this flow's first view state. The requireHardCopy view state pauses to ask the user whether she requires a CD of the purchase sent to her or a friend as a gift. Listing 9-24 shows the code for the requireHardCopy view state.

Listing 9-24. The requireHardCopy View State

requireHardCopy {
    on('yes') {
        if(!flow.shippingAddresses)
            flow.shippingAddress = new Address()
    }.to 'enterShipping'
    on('no') {
        flow.shippingAddress = null
    }.to 'loadRecommendations'
}

Notice that the requireHardCopy state specifies two event handlers called yes and no reflecting the potential answers to the question. Let's see how you can define a view that triggers these events. First create a GSP file called grails-app/views/store/buy/requireHardCopy.gsp.

Remember that the requireHardCopy.gsp file name should match the state name, and that the file should reside within a directory that matches the flow id—in this case, grails-app/views/store/buy. You will need to use the <g:link> tag's event attribute to trigger the events in the requireHardCopy state, as discussed previously in the section on triggering events from the view. Listing 9-25 shows the code to implement the requireHardCopy view state.

Listing 9-25. The requireHardCopy.gsp View

<g:applyLayout name="storeLayout">
    <div id="shoppingCart" class="shoppingCart">
        <h2>Would you like a CD edition of the album
                   sent to you or a friend as a gift?</h2>
        <div class="choiceButtons">
            <g:link controller="store" action="buy" event="yes">
                <img src="${createLinkTo(dir:'images',file:'yes-button.gif')}"
                          border="0"/>
            </g:link>
            <g:link controller="store" action="buy" event="no">
                <img src="${createLinkTo(dir:'images',file:'no-button.gif')}"
                          border="0"/>
            </g:link>
        </div>
    </div>
</g:applyLayout>

Notice how you can leverage the storeLayout once again to make sure the user interface remains consistent. Each <g:link> tag uses the event attribute to specify the event to trigger. Figure 9-3 shows what the dialog looks like.

image

Figure 9-3. Choosing whether you want a CD hard copy

As you can see from the requireHardCopy state's code in Listing 9-24, if a yes event is triggered, the flow will transition to the enterShipping state; otherwise it will head off to the loadRecommendations state. Each of these states will help you learn a little more about how flows work. Let's look at the enterShipping state, which presents a good example of doing data binding and validation.

Data Binding and Validation in Action

The enterShipping state is the first view state that asks the user to do some form of free-text entry. As soon as you start to accept input of this nature from a user, the requirement to validate input increases. Luckily, you've already specified the necessary validation constraints on the Address class in Listing 9-13. Now it's just a matter of putting those constraints to work.

Look at the implementation of the enterShipping state in Listing 9-26. As you can see, it defines two event handlers called next and back.

Listing 9-26. The enterShipping State

1 enterShipping {
2    on('next') {
3        def address = flow.shippingAddress
4        address.properties = params
5        if(address.validate()) {
6            flow.lastAlbum.shippingAddress = address
7            return success()
8         }
9         return error()
10     }.to 'loadRecommendations'
11     on('back') {
12         flow.shippingAddress.properties = params
13     }.to 'requireHardCopy'
14 }

We'll revisit the transition actions defined for the next and back events shortly. For the moment, let's develop the view that will render the enterShipping state and trigger each event. Create a GSP at the location grails-app/views/store/buy/enterShipping.gsp. Again, you can use the storeLayout to ensure the layout remains consistent. Listing 9-27 shows a shortened version of the code because the same <g:textField> tag is used for each property of the Address class.

Listing 9-27. The enterShipping.gsp View

1 <g:applyLayout name="storeLayout">
2    <div id="shoppingCart" class="shoppingCart">
3        <h2>Enter your shipping details below:</h2>
4        <div id="shippingForm" class="formDialog">
5            <g:hasErrors bean="${shippingAddress}">
6                <div class="errors">
7                    <g:renderErrors bean="${shippingAddress}"></g:renderErrors>
8                </div>
9            </g:hasErrors>
10
11           <g:form name="shippingForm" url="[controller:'store',action:'buy']">
12                   <div class="formFields">
13                       <div>
14                           <label for="number">House Name/Number:</label><br>
15                           <g:textField name="number"
16                                       value="${fieldValue(bean:shippingAddress,
17                                                           field:'number')}" />
18                       </div>
19                       <div>
20                           <label for="street">Street:</label><br>
21                           <g:textField name="street"
22                                  value="${fieldValue(bean:shippingAddress,
23                                                      field:'street')}" />
24                      </div>
25                  </div>
26                  ....
27                  <div class="formButtons">
28                      <g:submitButton type="image"
29                               src="${createLinkTo(dir:'images',
30                                                   file:'back-button.gif')}"
31                               name="back"
32                               value="Back"></g:submitButton>
33                      <g:submitButton type="image"
34                               src="${createLinkTo(dir:'images',
35                                                   file:'next-button.gif')}"
36                               name="next"
37                               value="Next"></g:submitButton>
38                  </div>
39
40
41           </g:form>
42        </div>
43    </div>
44</g:applyLayout>

After creating fields for each property in the Address class, you should end up with something that looks like the screenshot in Figure 9-4.

image

Figure 9-4. Entering shipping details

As discussed in the previous section on triggering events from the view, the name of the event to trigger is established from the name attribute of each <g:submitButton>. For example, the following snippet taken from Listing 9-27 will trigger the next event:

33    <g:submitButton type="image"
34               src="${createLinkTo(dir:'images',
35                                   file:'next-button.gif')}"
36               name="next"
37               value="Next"></g:submitButton>

Another important part of the code in Listing 9-27 is the usage of <g:hasErrors> and <g:renderErrors> to deal with errors that occur when validating the Address:

5    <g:hasErrors bean="${shippingAddress}">
6        <div class="errors">
7            <g:renderErrors bean="${shippingAddress}"></g:renderErrors>
8        </div>
9    </g:hasErrors>

This code works in partnership with the transition action to ensure that the Address is validated before the user continues to the next part of the flow. You can see the transition action's code in the following snippet, taken from Listing 9-26:

2    on('next') {
3        def address = flow.shippingAddress
4        address.properties = params
5        if(address.validate()) {
6            flow.lastAlbum.shippingAddress = address
7            return success()
8        }
9        return error()
10    }.to 'loadRecommendations'

Let's step through this code line by line to better understand what it's doing. First, on line 3 the shippingAddress is obtained from flow scope:

3 def address = flow.shippingAddress

If you recall from Listing 9-24, in the requireHardCopy state you created a new instance of the Address class and stored it in a variable called shippingAddress in flow scope when the user specified that she required a CD version of the Album. Here, the code obtains the shippingAddress variable using the expression flow.shippingAddress. Next, the params object is used to bind incoming request parameters to the properties of the Address object on line 4:

4 address.properties = params

This will ensure the form fields that the user entered are bound to each property in the Address object. With that done, the Address object is validated through a call to its validate() method. If validation passes, the Address instance is applied to the shippingAddress property of the lastAlbum object stored in flow scope. The success event is then triggered by a call to the success() method. Lines 5 through 8 show this in action:

5    if(address.validate()) {
6        flow.lastAlbum.shippingAddress = address
7        return success()
8     }

Finally, if the Address object does not validate because the user entered data that doesn't adhere to one of the constraints defined in Listing 9-15, the validate() method will return false, causing the code to fall through and return an error event:

9 return error()

When an error event is triggered, the transition action will halt the transition to the loadRecommendations state, returning the user to the enterShipping state. The view will then render any errors that occurred so the user can correct her mistakes (see Figure 9-5).

image

Figure 9-5. Showing validation errors in the enterShipping state

One final thing to note about the enterShipping state is the back event, which allows the user to go back to the requireHardCopy state and change her decision if she is too daunted by our form:

11    on('back') {
12        flow.shippingAddress.properties = params
13    }.to 'requireHardCopy'

This code also has a transition action that binds the request parameters to the shippingAddress object, but here you don't perform any validation. Why? If you have a really indecisive user who changes her mind again and decides she does want a hard copy shipped to her, all of the previous data that she entered is restored. This proves to be a useful pattern, because no one likes to fill in the same data over and over again.

And with that, you've completed your first experience with data binding and validation in conjunction with web flows. In the next section, we're going to look at implementing a more interesting action state that interacts with GORM.

Action States in Action

The enterShipping state from the previous section transitioned to a new state called loadRecommendations once a valid Address had been entered. The loadRecommendations state is an action state that interacts with GORM to inspect the user's order and query for other albums she might be interested in purchasing.

Action states are perfect for populating flow data before redirecting flow to another state. In this case, we want to produce two types of recommendations:

  • Genre recommendations: We show recent additions to the store that share the same genre (rock, pop, alternative, etc.) as the album(s) the user is about to purchase.
  • "Other users purchased" recommendations: If another user has purchased the same Album the current user is about to purchase, then we show some of the other user's purchases as recommendations.

As you can imagine, both of the aforementioned recommendations will involve some interesting queries that will give you a chance to play with criteria queries—a topic we'll cover in more detail in Chapter 10. However, before we get ahead of ourselves, let's define the loadRecommendations action state as shown in Listing 9-28.

Listing 9-28. The loadRecommendations Action State

loadRecommendations {
    action {
         ...
    }
    on('success').to 'showRecommendations'
    on('error').to 'enterCardDetails'
    on(Exception).to 'enterCardDetails'
}

As you can see, the loadRecommendations action state defines three event handlers. Two of them use the all-too-familiar names success and error, whereas the other is an Exception event handler. The error and Exception handlers simply move the flow to the enterCardDetails state. The idea here is that errors that occur while loading recommendations shouldn't prevent the user from completing the flow.

Now let's implement the first of the recommendation queries, which involves querying for other recent albums of the same genre. To do this, you can use a criteria query, which is an alternative to String-based queries such as SQL or HQL (Hibernate Query Language).

String-based queries are inherently error-prone for two reasons. First, you must conform to the syntax of the query language you are using without any help from an IDE or language parser. Second, String-based queries lose much of the type information about the objects you are querying. Criteria queries offer a type-safe, elegant solution that bypasses these issues by providing a Groovy builder to construct the query at runtime.

To fully understand criteria queries, you should look at an example. Listing 9-29 shows the criteria query to find genre recommendations.

Listing 9-29. Querying for Genre Recommendations

1 if(!flow.genreRecommendations) {
2    def albums = flow.albumPayments.album
3    def genres = albums.genre
4    flow.genreRecommendations = Album.withCriteria {
5        inList 'genre', genres
6        not {
7            inList 'id', albums.id
8        }
9        maxResults 4
10       order 'dateCreated', 'desc'
11   }
12 }

Let's step through the code to understand what it is doing. First, a GPath expression is used to obtain a list of Album instances on Line 2:

2 def albums = flow.albumPayments.album

Remember that flow.albumPayments is a List, but through the expressiveness of GPath you can use the expression flow.albumPayments.album to get another List containing each album property from each AlbumPayment instance in the List. GPath is incredibly useful, so much so that it appears again on Line 3:

3 def genres = albums.genre

This GPath expression asks for all the genre properties for each Album instance. Like magic, GPath obliges. With the necessary query data in hand, you can now construct the criteria query using the withCriteria method on Line 4:

4 flow.genreRecommendations = Album.withCriteria {

The withCriteria method returns a List of results that match the query. It takes a closure that contains the criteria query's criterion, the first of which is inList on line 5:

5 inList 'genre', genres

What this code is saying here is that the value of the Album object's genre property should be in the List of specified genres, thus enabling queries for albums of the same genre. The next criterion is a negated inList criterion that ensures the recommendations you get back aren't any of the albums already in the List of AlbumPayment instances. Lines 6 through 8 show the use of the not method to negate any single criterion or group of criteria:

6          not {
7              inList 'id', albums.id
8          }

Finally, to ensure that you get only the latest four albums that fulfill the aforementioned criterion, you can use the maxResults and order methods on lines 9 and 10:

9       maxResults 4
10      order 'dateCreated', 'desc'

And with that, the loadRecommendations action state populates a list of genre-based recommendations into a genreRecommendations variable held in flow scope. Now let's look at the second case, which proves to be an even more interesting query. The query essentially figures out what albums other users have purchased that are not in the list of albums the current user is about to purchase (see Listing 9-30).

Listing 9-30. The User Recommendations Query

1 if(!flow.userRecommendations) {
2    def albums = flow.albumPayments.album
3
4    def otherAlbumPayments = AlbumPayment.withCriteria {
5        user {
6            purchasedAlbums {
7                inList 'id', albums.id
8            }
9        }
10         not {
11             eq 'user', flow.user
12             inList 'album', albums
13       }
14          maxResults 4
15    }
16    flow.userRecommendations = otherAlbumPayments.album
17 }

Let's analyze the query step-by-step. The first four lines are essentially the same as the previous query, except you'll notice the AlbumPayment class on line 4 instead of a query to the Album class:

4 def otherAlbumPayments = AlbumPayment.withCriteria {

Lines 5 through 9 get really interesting:

5         user {
6             purchasedAlbums {
7                 inList 'id', albums.id
8             }
9         }

Here, an interesting feature of Grails' criteria support lets you query the associations of a domain class. By using the name of an association as a method call within the criteria, the code first queries the user property of the AlbumPayment class. Taking it even further, the code then queries the purchasedAlbums association of the user property. In a nutshell, the query is asking, "Find me all the AlbumPayment instances where the User associated with the AlbumPayment has one of the albums I'm about to buy in their list of purchasedAlbums." Simple, really!

In this advanced use of criteria, there is also a set of negated criteria on lines 10 through 13:

10          not {
11              eq 'user', flow.user
12              inList 'album', albums
13          }

These two criteria guarantee two things. First, line 11 ensures that you don't get back AlbumPayment instances that relate to the current User. The logic here is that you want recommendations only from other users—not from the user's own purchases. Second, on line 12, the negated inList criterion ensures you don't get back any AlbumPayment instances that are the same as one of the albums the user is about to buy. No point in recommending that a user buy something she's already about to buy, is there?

With the query out the way, on line 16 a new variable called userRecommendations is created in flow scope. The assignment uses a GPath expression to obtain each album property from the list of AlbumPayment instances held in the otherAlbumPayments variable:

16 flow.userRecommendations = otherAlbumPayments.album

Now that you have populated the flow.userRecommendations and flow.genreRecommendations lists, you can check whether they contain any results. There is no point in showing users a page with no recommendations. The code in Listing 9-31 checks each variable for results.

Listing 9-31. Checking for Results in the loadRecommendations State

if(!flow.genreRecommendations && !flow.userRecommendations) {
    return error()
}

Remember that in Groovy, any empty List resolves to false. If there are no results in either the userRecommendations or the genreRecommendations list, the code in Listing 9-31 triggers the execution of the error event, which results in skipping the recommendations page altogether.

That's it! You're done. The loadRecommendations state is complete. Listing 9-32 shows the full code in action.

Listing 9-32. The Completed loadRecommendations State

loadRecommendations {
    action {
        if(!flow.genreRecommendations) {
            def albums = flow.AlbumPayments.album
            def genres = albums.genre
            flow.genreRecommendations = Album.withCriteria {
                inList 'genre', genres
                not {
                    inList 'id', albums.id
                }
                maxResults 4
                order 'dateCreated', 'desc'
            }
        }
        if(!flow.userRecommendations) {
            def albums = flow.AlbumPayments.album

            def otherAlbumPayments = AlbumPayment.withCriteria {
                user {
                    purchasedAlbums {
                        inList 'id', albums.id
                    }
                }
                not {
                    eq 'user', flow.user
                    inList 'album', albums
                }
                maxResults 4
            }
            flow.userRecommendations = otherAlbumPayments.album
       }
       if(!flow.genreRecommendations && !flow.userRecommendations) {
           return error()
       }

    }
    on('success').to 'showRecommendations'
    on('error').to 'enterCardDetails'
    on(Exception).to 'enterCardDetails'
}

You've completed the loadRecommendations action state. Now let's see how you can present these recommendations in the showRecommendations state. The following section will also show how you can easily reuse transition and action states using assigned closures.

Reusing Actions with Closures

Once the loadRecommendations action state has executed and successfully accumulated a few useful Album recommendations for the user to peruse, the next stop is the showRecommendations view state (see Listing 9-33).

Listing 9-33. The showRecommendations View State

1      showRecommendations {
2          on('addAlbum'){
3             if(!flow.AlbumPayments) flow.albumPayments = []
4             def album = Album.get(params.id)
5
6             if(!flow.albumPayments.album.find { it?.id == album.id }) {
7                 flow.lastAlbum = new AlbumPayment(album:album)
8                 flow.AlbumPayments << flow.lastAlbum
9             }
10         }.to 'requireHardCopy'
12         on('next').to 'enterCardDetails'
13         on('back').to{ flow.shippingAddress ? 'enterShipping' : 'requireHardCopy' }
14    }

Now, you might have noticed a striking similarity between the transition action for the addAlbum event and the transition action for the success event in Listing 9-18. Make no mistake: the code between those two curly brackets from lines 3 to 9 is identical to that shown in Listing 9-18.

In the spirit of DRY (Don't Repeat Yourself), you should never break out the copy machine when it comes to code. Repetition is severely frowned upon. So how can you solve this criminal coding offense? The solution is simple if you consider how Groovy closures operate.

Closures in Groovy are, of course, first-class objects themselves that can be assigned to variables. Therefore you can improve upon the code in Listing 9-31 by extracting the transition action into a private field as shown in Listing 9-34.

Listing 9-34. Using a Private Field to Hold Action Code

private addAlbumToCartAction = {
    if(!flow.albumPayments) flow.albumPayments = []
    def album = album.get(params.id)

    if(!flow.AlbumPayments.album.find { it?.id == album.id }) {
        flow.lastAlbum = new AlbumPayment(album:album)
        flow.AlbumPayments << flow.lastAlbum
    }
}

With this done, you can refactor both the success event from the start state and the addAlbum event from the showRecommendations view state as shown in Listing 9-35, highlighted in bold.

Listing 9-35. Reusing Closure Code in Events

def buyFlow = {
    start {
        ...
        on('success', addAlbumToCartAction).to 'requireHardCopy'
    }
    ...
    showRecommendations {
        on('addAlbum', addAlbumToCartAction).to 'requireHardCopy'
        on('next').to 'enterCardDetails'
        on('back').to { flow.shippingAddress ? 'enterShipping' : 'requireHardCopy' }
    }
}

With that done, the showRecommendations state is a lot easier on the eye. As you can see, it defines three events: addAlbum, next, and back. The addAlbum event uses a transition action to add the selected Album to the list of albums the user wishes to purchase. It then transitions back to the requireHardCopy state to inquire if the user wants a CD version of the newly added Album.

The next event allows the user to bypass the option of buying any of the recommendations and go directly to entering her credit-card details in the enterCardDetails state. Finally, the back event triggers the first example of a dynamic transition, a topic that we'll cover later in the chapter.

Now all you need to do is provide a view to render the recommendations and trigger the aforementioned states. Do this by creating a file called grails-app/views/store/buy/showRecommendations.gsp that once again uses the storeLayout. Listing 9-36 shows the code for the showRecommendations.gsp file.

Listing 9-36. The showRecommendations.gsp View

<g:applyLayout name="storeLayout">
    <div id="shoppingCart" class="shoppingCart">
        <h2>Album Recommendations</h2>
        <g:if test="${genreRecommendations}">
            <h3>Other music you might like...</h3>
            <g:render template="/store/recommendations"
                      model="[albums:genreRecommendations]" />
        </g:if>
        <g:if test="${userRecommendations}">
            <h3>Other users who bought ${albumPayments.album} also bought...</h3>
            <g:render template="/store/recommendations"
                      model="[albums:userRecommendations]" />
        </g:if>
        <div class="formButtons">
            <g:link controller="store" action="buy" event="back">
               <img src="${createLinkTo(dir:'images',file:'back-button.gif')}"
                         border="0">
            </g:link>
            <g:link controller="store" action="buy" event="next">
               <img src="${createLinkTo(dir:'images',file:'next-button.gif')}"
                         border="0">
            </g:link>
        </div>
    </div>
</g:applyLayout>

The showRecommendations.gsp view contains two <g:link> tags that trigger the next and back events. It then uses an additional template located at grails-app/views/store/_recommendations.gsp to render each list of recommendations. The code for the _recommendations.gsp is shown in Listing 9-37.

Listing 9-37. The _recommendations.gsp Template

<table class="recommendations">
    <tr>
        <g:each in="${albums?}" var="album" status="i">
            <td>
            <div id="rec${i}" class="recommendation">
                <g:set var="header">${album.artist.name} - ${album.title}</g:set>
                <p>
                  ${header.size() >15 ? header[0..15] + '...' : header }
                </p>
                <music:albumArt width="100"
                                album="${album}"
                                artist="${album.artist}" />
                <p><g:link controller="store"
                           action="buy"
                           id="${album.id}"
                           event="addAlbum">Add to Purchase</g:link></p>
            </div>
            </td>
        </g:each>
    </tr>
</table>

The important bit of this template is the "Add to Purchase" <g:link> that triggers the addAlbum event, passing in the Album id. All in all, once users start purchasing albums, they'll start to see recommendations appearing in the flow as presented in Figure 9-6.

image

Figure 9-6. Recommending albums to users

Using Command Objects with Flows

Once users get through the recommendation system, they arrive at the business end of the transaction where they have to enter their credit-card details.

Tip If you're security-aware, you will note that it's generally not advisable to take user information, especially credit-card details, over HTTP. To run Grails in development mode over HTTPS, use the grails run-app-https command. At deployment time, your container can be configured to deliver parts of your site over HTTPS.

To start off, define a view state called enterCardDetails as shown in Listing 9-38.

Listing 9-38. Defining the enterCardDetails View State

enterCardDetails {
   ...
}

Before you can start capturing credit-card details, you need to set up an appropriate form that the user can complete. You can accomplish this by creating a new view at the location grails-app/views/store/buy/enterCardDetails.gsp, which the enterCardDetails view state can render. Listing 9-39 shows the enterCardDetails.gsp view simplified for brevity.

Listing 9-39. The enterCardDetails.gsp View State

<g:applyLayout name="storeLayout">
<div id="shoppingCart" class="shoppingCart">
    <h2>Enter your credit card details below:</h2>
    <div id="shippingForm" class="formDialog">
        <g:form name="shippingForm" url="[controller:'store',action:'buy']">
            <div class="formFields">
                <div>
                    <label for="name">Name on Card:</label><br>
                    <g:textField name="name"
                                 value="${fieldValue(bean:creditCard,
                                                     field:'name')}" />
                </div>
                    ...

            </div>
            <div class="formButtons">
                <g:submitButton name="back"
                                value="Back" />
                <g:submitButton name="next"
                                value="Next" />
            </div>
        </g:form>
    </div>
</div>
</g:applyLayout>

Figure 9-7 shows what the final view rendering looks like after all the necessary form fields have been added.

image

Figure 9-7. The enterCreditCard.gsp form

Now let's consider how to capture the credit-card information from the user. A domain class doesn't really make sense because you don't want to persist credit-card information at this point. Luckily, like regular controller actions, flow actions support the concept of command objects first discussed in Chapter 4.

First you need to define a class that represents the command object. Listing 9-40 shows the code for the CreditCardCommand class.

Listing 9-40. A CreditCardCommand Class Used as a Command Object

class CreditCardCommand implements Serializable {
    String name
    String number
    String expiry
    Integer code
    static constraints = {
        name blank:false, minSize:3
        number creditCard:true, blank:false
        expiry matches:/d{2}/d{2}/, blank:false
        code nullable:false,max:999
    }
}

Like domain classes, command objects support the concept of constraints. Grails even provides a creditCard constraint to validate credit-card numbers. Within the messages.properties file contained in the grails-app/i18n directory, you can provide messages that should be displayed when the constraints are violated. Listing 9-41 presents a few example messages.

Tip If you're not a native English speaker, you could try providing messages in other languages. You could use messages_es.properties for Spanish, for example, as you learned in Chapter 7 on internationalization (i18n).

Listing 9-41. Specifying Validation Messages for the CreditCardCommand Object

creditCardCommand.name.blank=You must specify the name on the credit card
creditCardCommand.number.blank=The credit card number cannot be blank
creditCardCommand.number.creditCard.invalid=You must specify a valid card number creditCardCommand.code.nullable=Your must specify the security code
creditCardCommand.expiry.matches.invalid=You must specify the expiry. Example 05/10
creditCardCommand.expiry.blank=You must specify the expiry number

Now let's use the CreditCardCommand command object to define the next event and associated transition action that will validate the credit-card details entered by the user. Listing 9-42 shows how easy it is.

Listing 9-42. Using the CreditCardCommand Command Object

enterCardDetails {
    on('next') { CreditCardCommand cmd ->
        flow.creditCard = cmd
        cmd.validate() ? success() : error()
    }.to 'showConfirmation'
}

If you simply define the command object as the first parameter to the closure passed as the transition action, Grails will automatically populate the command instance from the parameters in the request. The only thing left for you to do is validate the command object using the validate() method and trigger a success or error event.

You'll notice that in addition to the validation of the command object in Listing 9-42, the command object is placed into flow scope through the variable name creditCard. With that done, you can update the enterCardDetails.gsp view first shown in Listing 9-39 to render any error messages that occur. The changes to enterCardDetails.gsp are shown in bold in Listing 9-43.

Listing 9-43. Displaying Error Messages from a Command Object

<g:applyLayout name="storeLayout">
    <div id="shoppingCart" class="shoppingCart">
        <h2>Enter your credit card details below:</h2>
        <div id="shippingForm" class="formDialog">
            <g:hasErrors bean="${creditCard}">
                <div class="errors">
                    <g:renderErrors bean="${creditCard}"></g:renderErrors>
         </div>
     </g:hasErrors>
     ...
   </div>
</div>
</g:applyLayout>

Figure 9-8 shows the error messages being rendered to the view. You'll probably need to use one of your own credit cards to get past Grails' credit-card number validator!

image

Figure 9-8. Validating credit card details

Dynamic Transitions

Before we move on from the enterCardDetails view state, you need to implement the back event that allows the user to return to the previous screen. Using a static event name to transition back to the showRecommendations event doesn't make sense because there might not have been any recommendations. Also, if the user wanted the Album to be shipped as a CD, then the previous screen was actually the enterShipping view state!

In this scenario, you need a way to dynamically specify the state to transition to, and luckily Grails' Web Flow support allows dynamic transitions by using a closure as an argument to the to method. Listing 9-44 presents an example of a dynamic transition that checks whether there are any recommendations, and transitions back to the showRecommendations state if there are. Alternatively, if there are no recommendations and the lastAlbum purchased has a shipping Address, the dynamic transition goes back to the enterShipping state. Otherwise, it goes back to the requireHardCopy state.

Listing 9-44. Using Dynamic Transitions to Specify a Transition Target State

enterCardDetails {
   ..
   on('back').to {
       def view
       if(flow.genreRecommendations || flow.userRecomendations)
          view = "showRecommendations"
       else if(flow.lastAlbum.shippingAddress) {
           view = 'enterShipping'
       }
       else {
           view = 'requireHardCopy'
       }
       return view
    }
}

Notice how the name of the view to transition to is the return value of the closure passed to the to method. In other words, the following three examples are equivalent, with each transitioning to the enterShipping state:

on('back').to 'enterShipping' // static String name
on('back').to { 'enterShipping' } // Groovy optional return
on('back').to { return 'enterShipping' } // Groovy explicit return

Verifying Flow State with Assertions

Okay, you're on the home stretch. You've reached the showConfirmation view state, which is the final view state that engages the user for input. Listing 9-45 shows the GSP code for the showConfirmation.gsp view.

Listing 9-45. The showConfirmation.gsp View

<g:applyLayout name="storeLayout">
    <div id="shoppingCart" class="shoppingCart">
        <h2>Your Purchase</h2>
        <p>You have the following items in your cart that you wish to Purchase. </p>
        <ul>
           <g:each in="${albumPayments}" var="AlbumPayment">
           <li>${albumPayment.album.artist.name} - ${albumPayment.album.title}
                   <br>
                   <strong>Cost: </strong> $      ${albumPayment.album.price}
            </li>
        </g:each>

    </ul>
    <g:set var="totalAmount">
            <g:formatNumber
                             number="${AlbumPayments.album.price.sum()}"
                             format="0.00" /></g:set>
    <p><strong>Total:</strong> $ ${totalAmount}</p>

    <h2>Card Details</h2>
    <p>The following card details will be used to process this transaction:</p>
    <div class="cardDetails">
        <ul>
            <li><strong>Name:</strong> ${creditCard?.name}</li>
            <li><strong>Number:</strong> ${creditCard?.number}</li>
            <li><strong>Expiry:</strong> ${creditCard?.expiry}</li>
            <li><strong>Security Code:</strong> ${creditCard?.code}</li>
        </ul>

    </div>
    <div class="formButtons">
        <g:link controller="store" action="buy" event="back">
           <img src="${createLinkTo(dir:'images',file:'back-button.gif')}"
                border="0">
        </g:link>
        <g:link controller="store" action="buy" event="confirm">
             <img src="${createLinkTo(dir:'images',file:'confirm-button.gif')}"
              border="0">
        </g:link>
    </div>
</div>
</g:applyLayout>

When rendered, the showConfirmation view will display a summary of the transaction the user is about to complete, including all the albums to be purchased, the total price, and the credit-card details. Figure 9-9 shows the showConfirmation view in all its glory.

image

Figure 9-9. Confirming the user's purchase

So the user can trigger one of two events from the showConfirmation view state: confirm or back. The confirm event is where you can implement our transaction processing. To keep the example simple (both in terms of code brevity and later distribution), we're not going to delve into implementing a true e-commerce solution. We'll just happily assume that payments go through without a hitch.


Tip If you want to integrate an e-commerce solution, try the PayPal plugin for Grails at http://grails.org/Paypal+Plugin.


The confirm state, however, will help you learn how to use assertions inside a flow definition to validate flow state. Remember: by the time the user clicks the "confirm" button, the flow should be in the correct state. If it is not, you probably have an error in your code, which is something assertions exist to solve. Listing 9-46 shows the confirm event and the transition action that deals with taking payments (or not, as the case may be).

Listing 9-46. Using a Transition Action to Confirm the Purchase

1 showConfirmation {
2    on('confirm') {
3        def user = flow.user
4        def albumPayments = flow.albumPayments
5        def p = new Payment(user:user)
6        flow.payment = p
7        p.invoiceNumber = "INV-${user.id}-${System.currentTimeMillis()}"
8        def creditCard = flow.creditCard
9        assert creditCard.validate()
10        // TODO: Use credit card to take payment
11       // ...
12
13       // Once payment taken update user profile
14       for(ap in albumPayments) {
15           ap.user = user
16           // validation should never fail at this point
17           assert ap.validate()
18
19           p.addToAlbumPayments(ap)
20           assert p.save()
21
22           ap.album.songs.each { user.addToPurchasedSongs(it) }
23           user.addToPurchasedAlbums(ap.album)
24           assert user.save(flush:true)
25       }
26    }.to 'displayInvoice'
27  ...
28 }

On lines 9, 17, 20, and 24 assertions are used via Groovy's built-in assert keyword, to validate the state of the flow. You should not be getting validation errors by the time the flow reaches this transition action; if you are, there is a problem with the code in the flow prior to the showConfirmation view state.

As for the rest of the code in Listing 9-46, on lines 5 through 7 a new Payment instance is created, placed in the flow, and assigned a generated invoice number:

5       def p = new Payment(user:user)
6       flow.payment = p
7       p.invoiceNumber = "INV-${user.id}-${System.currentTimeMillis()}"

Then on line 10, there is a "to-do" item for when you actually start processing credit-card details. Once the credit card has been processed, on lines 19 and 20 each AlbumPayment is added to the Payment instance, which is then saved through a call to the save() method:

19         p.addToAlbumPayments(ap)
20         assert p.save()

Finally, on lines 22 through 24 the User is updated. The code adds each of the songs from the Album purchased to her list of purchasedSongs and adds the Album itself to her list of purchasedAlbums:

22         ap.album.songs.each { user.addToPurchasedSongs(it) }
23         user.addToPurchasedAlbums(ap.album)
24         assert user.save(flush:true)

If all goes well, the confirm event of the showConfirmation view state will transition to the displayInvoice end state. The displayInvoice end state will attempt to render a view located at grails-app/views/store/buy/displayInvoice.gsp. The displayInvoice.gsp view is a simple GSP that displays a summary of the user's just-processed transaction along with her invoice number for future reference. For completeness, we've included the code for displayInvoice.gsp (see Listing 9-47).

Listing 9-47. The displayInvoice.gsp End State View

<g:applyLayout name="storeLayout">
<g:applyLayout name="storeLayout">
    <div id="invoice" class="shoppingCart">
        <h2>Your Receipt</h2>
        <p>Congratulations you have completed your purchase.
               Those purchases that included shipping will ship
               within 2-3 working days.
               Your digital purchases have been transferred into your library</p>
        <p>Your invoice number is ${payment.invoiceNumber}</p>
        <h2>Purchased Items</h2>

            <ul>
                <g:each in="${albumPayments}" var="albumPayment">
                    <li>${albumPayment.album.artist.name} -
                           ${albumPayment.album.title}<br>
                           <strong>Cost: </strong> $     ${albumPayment.album.price}
                   </li>
                </g:each>

            </ul>
        <p><strong>Total: </strong> ${albumPayments.album.price.sum()}</p>
    </div>
</g:applyLayout>

The user will see the unusually short invoice displayed, as depicted in Figure 9-10 (the legal department hasn't got hold of it yet).

image

Figure 9-10. Displaying an invoice

And with that, you have completed the gTunes checkout flow! Listing 9-48 shows the code for the complete flow you developed over the course of this chapter.

Listing 9-48. The Finished buyFlow Code

def buyFlow = {
    start {
        action {
            // check login status
            if(session.user) {
                flow.user = User.get(session.user.id)
                return success()
           }
           login()
         }
         on('success', addAlbumToCartAction).to 'requireHardCopy'
         on('login') {
             flash.album = Album.get(params.id)
             flash.message = "user.not.logged.in"
        }.to 'requiresLogin'
    }
    requireHardCopy {
        on('yes') {
            if(!flow.shippingAddresses)
                flow.shippingAddress = new Address()
         } .to 'enterShipping'
         on('no') {
             flow.shippingAddress = null
         }.to 'loadRecommendations'
      }
      enterShipping {
         on('next') {
             def address = flow.shippingAddress
             address.properties = params
             if(address.validate()) {
                 flow.lastAlbum.shippingAddress = address
                 return success()
             }
             return error()
         }.to 'loadRecommendations'
         on('back') {
             flow.shippingAddress.properties = params
           }.to 'requireHardCopy'
         }
         loadRecommendations {
             action {
                 if(!flow.genreRecommendations) {
                     def albums = flow.albumPayments.album
                     def genres = albums.genre
                     flow.genreRecommendations = Album.withCriteria {
                         inList 'genre', genres
                         not {
                             inList 'id', albums.id
                        }
                        maxResults 4
                        order 'dateCreated', 'desc'
                      }
                  }
                  if(!flow.userRecommendations) {
                      def albums = flow.albumPayments.album
                      def otherAlbumPayments = AlbumPayment.withCriteria {
                          user {
                              purchasedAlbums {
                                  inList 'id', albums.id
                              }
                         }
                         not {
                             eq 'user', flow.user
                             inList 'album', albums
                         }
                         maxResults 4
                     }
                     flow.userRecommendations = otherAlbumPayments.album
                  }
                  if(!flow.genreRecommendations && !flow.userRecommendations) {
                      return error()
                  }
           }
           on('success').to 'showRecommendations'
           on('error').to 'enterCardDetails'
           on(Exception).to 'enterCardDetails'
       }
       showRecommendations {
           on('addAlbum', addAlbumToCartAction).to 'requireHardCopy'
           on('next').to 'enterCardDetails'
           on('back').to { flow.shippingAddress ? 'enterShipping' : 'requireHardCopy' }
       }
       enterCardDetails {
           on('next') { CreditCardCommand cmd ->
               flow.creditCard = cmd
               cmd.validate() ? success() : error()
           }.to 'showConfirmation'
           on('back').to {
               def view
               if(flow.genreRecommendations || flow.userRecomendations)
                   view = "showRecommendations"
               else if(flow.lastAlbum.shippingAddress) {
                   view = 'enterShipping'
                   }
            else {
                view = 'requireHardCopy'
            }
            return view
        }
    }
    showConfirmation {
        on('confirm') {
            def user = flow.user
            def albumPayments = flow.albumPayments
            def p = new Payment(user:user)
            flow.payment = p
            p.invoiceNumber = "INV-${user.id}-${System.currentTimeMillis()}"
            def creditCard = flow.creditCard
            assert creditCard.validate()
            // TODO: Use credit card to take payment
            // ...

           // Once payment taken update user profile
           for(ap in albumPayments) {
               ap.user = user
               // validation should never fail at this point
               assert ap.validate()

               p.addToAlbumPayments(ap)
               assert p.save(flush:true)

               ap.album.songs.each { user.addToPurchasedSongs(it) }
               user.addToPurchasedAlbums(ap.album)
               assert user.save(flush:true)
           }
        }.to 'displayInvoice'
        on('back').to 'enterCardDetails'
        on('error').to 'displayError'
        on(Exception).to 'displayError'
    }
    requiresLogin {
        redirect(controller:"album",
                        action:"show",
                        id: flash.album.id,
                        params:[message:flash.message])
    }
    displayInvoice()
    displayError()
}

Testing Flows

As you've seen through the course of this chapter, flows deal with the specific challenge of taking the user through a multistep process. It should come as no surprise that in order to test flows, you must use a specialized test harness called grails.test.WebFlowTestCase. Throughout the remainder of this section, we'll show you how to use WebFlowTestCase to test flow interactions effectively.

As of this writing, WebFlowTestCase cannot be used in a regular unit test. So you need to create an integration test by running the create-integration-test command:

$ grails create-integration-test com.g2one.gtunes.StoreBuyFlow

You'll end up with a new test suite in the test/integration/com/g2one/gtunes directory called StoreBuyFlowTests.groovy. Currently, the StoreBuyFlowTests suite extends the vanilla GroovyTestCase superclass. You'll need to change it to extend the WebFlowTestCase test harness instead, as shown in Listing 9-49.

Listing 9-49. Extending the WebFlowTestCase Test Harness

package com.g2one.gtunes
import grails.test.*
class StoreBuyFlowTests extends WebFlowTestCase {
    ...
}

The next thing to do is provide an implementation of the abstract getFlow() method that returns a closure that represents the flow. Listing 9-50 shows how this is done for the buyFlow you developed earlier.

Listing 9-50. Implementing the getFlow() Method

class StoreBuyFlowTests extends WebFlowTestCase {
    ...
    def controller = new StoreController()
    def getFlow() {
        controller.buyFlow
    }
}

Now it's time to consider the first test to implement. Recall from Listing 9-18 that if the user is not logged in, the flow ends by sending a redirect in the requiresLogin end state. Listing 9-51 shows how to test whether a user is logged in.

Listing 9-51. Testing if the User is Logged In

1 void testNotLoggedIn() {
2 MockUtils.mockDomain(Album, [new Album(title:"Aha Shake Heartbreak", id:1L)])
3 controller.params.id = 1
4 startFlow()
5
6 assertFlowExecutionEnded()
7 assertFlowExecutionOutcomeEquals 'requiresLogin'
8 }

We've demonstrated a few key concepts in this simple test. First, notice how you can use the mockDomain method of MockUtils to provide some mock data on line 2:

2 MockUtils.mockDomain(Album, [new Album(title:"Aha Shake Heartbreak", id:1L)])

Then on line 3, you can specify the id of the Album instance that will be looked up in the start action of the buyFlow:

3 controller.params.id = 1

In this example, we're using an id that matches the id of the mock Album passed to the mockDomain method on line 2. Then to trigger flow execution, you can invoke the startFlow() method on line 4:

4 startFlow()

With one simple method, the flow execution will begin and proceed to execute the start state of the buyFlow. If you recall, the start state is an action state that checks whether the user is logged in by inspecting whether a User instance exists in the session object. If a user doesn't exist in the session object, the requiresLogin state is triggered, which terminates the flow and redirects to another action. Line 6 checks that the flow has been terminated, by calling the assertFlowExecutionEnded method:

6 assertFlowExecutionEnded()

On line 7 the assertFlowExecutionOutcomeEquals method is called to ensure that requiresLogin is the end state of the flow:

7 assertFlowExecutionOutcomeEquals 'requiresLogin'

As you can see, the WebFlowTestCase test harness provides a number of new methods, such as startFlow and assertFlowExecutionEnded, that allow you to manipulate flow execution. The following list summarizes the key extensions to the GroovyTestCase API and what they do:

  • assertFlowExecutionActive(): Asserts that the flow hasn't terminated by reaching an end state
  • assertFlowExecutionEnded(): Asserts that the flow has been terminated by reaching an end state
  • assertFlowExecutionOutcomeEquals(String): Asserts that the outcome (the name of the end state) is equal to the given value
  • assertCurrentStateEquals(String): Asserts that the current state in an active flow is equal to the specified value
  • startFlow(): Starts a new flow execution
  • signalEvent(String): Triggers a flow-execution event for the given name when the flow has paused (for example, at a view state)
  • setCurrentState(String): Starts a flow execution and sets the current state to the specified value

Of course, all the regular JUnit assertions are available in addition to those we just mentioned. One key method is the setCurrentState(String) method, which allows you to easily test just parts of a flow. For example, say you wanted to test only the process of entering the shipping address for a purchased album. Listing 9-52 shows how to use the setCurrentState(String) method to move the flow forward to a particular point.

Listing 9-52. Using setCurrentState(String) to Transition to a Particular State

void testEnterShippingAddress() {
     currentState = "requireHardCopy"
     signalEvent "yes"
     assertCurrentStateEquals "enterShipping"
     signalEvent "back"
     assertCurrentStateEquals "requireHardCopy"
     signalEvent "yes"
     assertCurrentStateEquals "enterShipping"
     signalEvent "next"
     assertCurrentStateEquals "enterShipping"
     ...
}

The example in Listing 9-52 sets the currentState to requireHardCopy. The next trick is to use the signalEvent(String) method to transition from one state to the next. Notice how the code in Listing 9-52 uses signalEvent(String) followed by assertCurrentStateEquals(String) to assert that the web flow is transitioning states as expected.

Now note one of the other aspects of the enterShipping state from Listing 9-26: it presents an example of using data binding to populate the Address class. In Listing 9-52, when you try to trigger the next event, the flow transitions back to the enterShipping state because the Address object doesn't validate. Listing 9-53 shows how to build on testEnterShippingAddress to test whether a valid Address object is provided.

Listing 9-53. Testing Data Binding in Flows

1 void testEnterShippingAddress() {
2 ...
3 signalEvent "next"
4 assertCurrentStateEquals "enterShipping"
5 def model = getFlowScope()
6
7 def errors = model.shippingAddress?.hasErrors()
8 assertNotNull errors
9 assertTrue errors
10
11 model.lastAlbum = new AlbumPayment(album:new Album(title:"Aha Shake Heartbreak"))
12 model.albumPayments = [model.lastAlbum]
13 controller.params.number = "10"
14 controller.params.street = "John Doe Street"
15 controller.params.city = "London"
16 controller.params.state = "Greater London"
17 controller.params.postCode = "W134G"
18 controller.params.country = "United Kingdom"
19 assertNotNull model.shippingAddress
20 signalEvent "next"
21 assertCurrentStateEquals "enterCardDetails"
22 def shippingAddress = model.shippingAddress
23
24 assertNotNull shippingAddress
25 assertTrue shippingAddress.validate()
26 }

The example in Listing 9-53 shows a number of useful techniques. First, it tests failed validation by triggering the next event (without populating parameters for the Address object) and obtaining the model from flow scope using the getFlowScope() method on lines 5 through 9. Then on lines 13 to 18, you can see how to use the controller instance to populate the params object in order for data binding to work effectively. The result is that when the next event is triggered, validation succeeds and state transitions to the enterCardDetails state instead of back to enterShipping.

As you develop the test coverage for your flows, you might need to mock out data-access methods provided by GORM as described in Chapter 10, but overall WebFlowTestCase makes testing flows a lot easier through the provided utility methods.

Summary

Don't be shocked: you have in fact reached the end of the web flow chapter! You've learned a great deal, from the different state types—action, view, redirect—to events and transitions. Along the way, you've completed a pretty comprehensive web flow example by implementing the checkout process for the gTunes application.

We should point out one thing: web flow is not the answer to every problem. For the majority of tasks you face on a day-to-day basis, regular controllers and actions do the job just fine. There's a particular subset of problems that involve taking the user through sequential steps that cannot be compromised, and that's where web flow really shines.

A couple of this chapter's examples in which you used criteria queries touched on the untapped power that is GORM. Never fear; our journey into the world of GORM has only just begun. In the next chapter, we'll jump head first into understanding what else GORM has to offer on the persistence front.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset