Having shared functionality/pages across software modules in web application development seems to be a common occurrence (at least within the projects on which I am working) and is a problem that can be handled in a number of ways. Currently, I happen to be implementing two different user modules in a Grails 2.0.x application that ended up needing such shared functionality, so I thought I would share my solution. I am going to describe how I initially developed the modules, and how I solved the problem of having a required common interstitial form page that is shared between the two or more page flows.

The first module

The page flow within the first module is as described in Figure 1 below:

When developing the module, both web page forms represented by the 2nd and 3rd steps in the flow were implemented within the same controller class (following the practice of Agile development of implementing what you need right now, and refactoring when circumstances require you to do so). Everything good so far, this is all standard web application development. Story complete, so let’s look at the second module.

The second module

The page flow within the second module is as described in Figure 2 below:

Note that the 2nd step in both Figure 1 and Figure 2 both note that some user input is required before the actual function can commence. As it turns out, the user input required is identical, meaning the exact same form data is captured from the user. This is depicted in Figure 3:

In “traditional” (read: lazy) web development, I might just copy all of the logic for displaying the form and capturing the input from the Option1 controller(s) within Module A into the Option 2 controller(s) within Module B. But I figured it could be implemented so much more cleanly without the need for any code duplication. The solution I decided upon was to implement the logic for determining the need to direct the user to the interstitial page within a Grails Filter.

The solution

Let’s take a look at the Filter class that determines whether the user is required to be directed to the interstitial page.

RequiredAttributeFilters.groovy

class RequiredAttributeFilters { def filters = { attributeRequiredforOptionA(controller: 'optionA', action: 'index') { before = { if(session.someAttribute == null) { // attribute not set, redirect user to collect data def targetUri = request.forwardURI.replace(request.contextPath, '') session.requiredAttributesFilterRedirectUri = targetUri redirect controller: 'requiredUserInput', action: 'index' } } } attributeRequiredforOptionB(controller: 'optionB', action: 'index') { before = { if(session.someAttribute == null) { // attribute not set, redirect user to collect data def targetUri = request.forwardURI.replace(request.contextPath, '') session.requiredAttributesFilterRedirectUri = targetUri redirect controller: 'requiredUserInput', action: 'index' } } } removeSomeAttribute(controller: 'user', action: 'menu') { before = { // user exited current function, remove required attribute session.someAttribute = null } } } }

This is just a standard Grails filter class that intercepts requests to “/option1/index” and “/option2/index” and redirects the user to “/requiredUserInput/index” if the value of session.someAttribute is null. Before redirection, it stores the value of the URL that user initially requested in the session. It also removes the attribute from session if the user exits current function (in my case, goes back to their menu). Now that we have the filter is place, let’s look at the controller that captures the required user input.

RequiredUserInputController.groovy

class RequiredUserInputController { def index = { // do stuff // render view render view: 'index' } def captureData = { // check form errors etc. def someAttribute = null; // get the data and set the attribute in session session.someAttribute = someAttribute redirect uri: session.requiredAttributesFilterRedirectUri } }

The above controller has two methods: the standard “index” method for rendering the form and the “captureData” method, which is called upon form submission. The latter performs any required form processing, and in addition, sets the value of session.someAttribute to the correct current value. It then redirects to the URL that was set in session by the filter that redirected here in the first place. The filter is called again, but session.someAttribute has a value so filter processing is skipped. I won’t detail the GSP code for the form page, but it contains a simple form whose action attribute points to the “captureData” URL.

That’s all there is to it. If in the future additional page flows need to same data in order to proceed, those controllers/actions/URIs just need to be added to the Filter class so that the interception occurs.