Chapter 29. Expresso Workflow

Table of Contents

Introduction
Theory
Controller & State Forms
Workflow
Controller Definition
State default flow
Controller Security & Chaining flow
Screen Build
State Run
Practice
Defining the workflow in the Controller
Defining the workflow at screen build time
Defining workflow at runtime
Workflow sample code
Conclusion
Contributors

Note

If you find this EDG documentation helpful please consider DONATING! to keep the doc alive and current.

 Maintainer:Aime Bazin

Workflow was added to Expresso to both facilitate and encourage a certain approach to developing screen flow logic. This approach expects controller objects to only perform ‘controller’ functions as described in the MVC design pattern.

Introduction

The following new features were added to Expresso to both facilitate and encourage a certain approach to developing screen flow logic. This approach expects controller objects to only perform ‘controller’ functions as described in the MVC design pattern. This should limit the controller to ‘adapt’ and ‘mediate’ type of actions. Thanks to the Controller class’ support for the HTTP protocol, there should be little need for web applications to provide their own ‘adapting’ code. This is because Expresso provides abstractions such as ControllerRequest and PersistentSession objects that have already done the adapting work between the HTTP and Java worlds.

Theory

In addition to being able to use transitions to transfer control between states and controllers, Expresso offers a basic form of workflow and allows for quite simple linear flows between states in a wizard like fashion. Each workflow state may also be re-useable in other controllers. This means that the workflow through the controller is defined at a higher level and not in the states themselves.

This approach expects controller objects to only perform 'controller' functions as described in the MVC design pattern. This should limit the controller to 'adapt' and 'mediate' type of actions. Thanks to the Controller class' support for the HTTP protocol, there should be little need for web applications to provide their own 'adapting' code. This is because Expresso provides abstractions such as ControllerRequest and PersistentSession objects that have already done the adapting work between the HTTP and Java worlds.

Most of the code required in Controller descendant classes should be solely for the purpose of providing ‘mediation’ logic. This is the logic that ties components of the application together. The new features that have been added to the Controller class encourage developers to keep this mediation logic inside controller objects by providing support for mediation between state objects. This should help to avoid mediation code finding its way into state classes.

Keeping the mediation code away from state classes allows these objects to focus on 'model' functions as described in MVC. In particular the state classes are 'commands' as defined in the Command design pattern. The state classes accept any required input via a StateForm at the time they are invoked, perform their 'business' logic and return any results via the state's form and/or the ControllerResponse object. If the business logic that needs to be performed is fairly complex then the state should act as a type of adapter. This is adapting between a user interaction and the classes used to support that user request (note: this is NOT like the adapting role of the Controller from HTTP to Java). The state would be responsible to take the single user request and adapt it into many calls onto supporting custom business 'model' objects. The state class (and any supporting model objects) should rarely include any mediation or workflow logic otherwise this reduces the potential for reuse in different application configurations. Therefore a properly coded state will be ignorant about who called it as well as who it should call once complete.

The workflow features were designed with the following intent: A controller should relate to a 'use-case'. This is a single business function such as 'Register to a website' or 'Add a new widget to the inventory'. These use cases could comprise either a single user interaction or many user interactions/screens. A state relates to a single user interaction such as 'Enter address information' where the address information fits on a single screen (Note: since we kept mediation logic out of the state, this same state could be used both within a wizard-style registration and as part of a stand-alone screen to revise address information).

When a controller is coded, states are added to the controller in one of 3 ways:

  • As a 'prompt' state

  • As a 'handle' state

  • As a 'final' state

Prompt states are those which build the contents of a screen using Input, Output, Block and Transition objects. The Controller will add its own transition objects based on the 'position' of the prompt state within the controller these are the Next and Previous buttons common in wizards. When errors are generated in a prompt state's associated handle state, the prompt state will be invoked by default assuming the handle state has not overridden this at runtime.

Handle states accept user input from a state form and usually perform validation on that data. These states are normally directly invoked as a result of a user action however they will also be invoked as a result of a 'final' state execution. Restricting these states to validation-type logic allows them to be reused more easily (eg Within a wizard or stand-alone). Even in a single-screen controller it is a good practice to separate the validation from the commit processing using a final state.

Note

Keeping the handle state ignorant of its associated prompt state allows it to be reused more readily. This can be useful for example, when automating a number of user tasks in a batch or proxy mode.

Final states are the last state to be invoked in a controller. They usually contain the guts of the processing involved in a use-case. This could include database updates or any other processing that needs to occur once all validation has succeeded. Before a final state is executed, all handle states within that controller are rerun and any validation errors cause the appropriate prompt state to be redisplayed. This is useful for wizard-style screen flow.

Structuring your states in this fashion allows a use-case to be fairly easily reconfigured so that it can be used via different user interfaces. In a WAP interface for example, a new controller would be created with states defined at a lower level of granularity. The final state could then be reused to complete the use-case operation.

Data collected through the wizard process may also be conveniently held in a ControllerForm which may be given request or session level scope. See the section on session data persistence for a detailed explanation.

Note: Use-cases with no user input form should be coded as separate controllers containing only a final state.

Controller & State Forms

The new features provide for the concept of both a state and controller form. The controller form holds all the data required by the final state to complete the use-case. The state form holds only the data required by the individual states to perform their duties. Controller forms should inherit from com.jcorporate.expresso.core.controller.ControllerForm and state forms should implement the com.jcorporate.expresso.core.controller.StateForm interface.

  • Controller form: The ActionForm defined in struts-config.xml for the given controller

  • State form: A bean that implements the StateForm interface

The state form defines the contract between the controller and the state. A state's form is specified using the addStatePairing() method as described below. It is populated from the controller form just prior to invoking the state. A new method has been defined within the State class called 'perform()'. This method replaces the existing run() method and should therefore be overridden to define state logic. This method has an additional parameter of type StateForm. Once the perform() has completed, the updated state form data is put back into the controller form. In a wizard setup, the attributes of the state form would be made up of a subset of the attributes on the controller form - this helps isolate the state from the other states in the controller. In a single screen controller, the state form would be the actual controller form.

The controller form holds all the data required to perform the use-case. For wizard controllers, the form would normally be kept in session scope as defined in the Struts config file. In these controllers, the controller form will accumulate more data as the user progresses through the screens. In single screen situations, the controller form is used as the state form.

Struts calls an ActionForm's reset() method every time it is populated from a user request. This would cause our data to be wiped out so the reset() method should not be overridden. Instead the resetController() method should be overridden. This method is only called when the initial state of a controller is invoked. The initial state is automatically defined when the controller's states are added in the controller constructor.

Workflow

The new features to support workflow capabilities revolve around the ability to direct execution between states and controllers. A new method, enableReturnToSender(), has been added to the Transition class and special Transition parameters can also be added to direct traffic.

Calling the enableReturnToSender() method will indicate that the destination controller/state as specified in the transition object should return control back to this source state once the destination controller/state has completed without errors. If the target state is in a different controller and is also a wizard controller then the flow does not return to the source state until the successful completion of all the wizard screens. If the target state is in the current controller then flow returns to the source state after the target state has completed without errors. In either case, when the source state is eventually re-invoked, it will have its parameter context reestablished.

An alternative to return-to-sender allows a transition to be defined with one of the following special workflow parameter pairs (as defined in the Controller class):

CTL_SUCC_CTL and CTL_SUCC_STATE STATE_SUCC_CTL and STATE_SUCC_STATE STATE_ERR_CTL and STATE_ERR_STATE

CTL_SUCC_XXXX - indicate to the target controller where it should transition to once the final state has completed successfully. This should always be used for transitions between different controllers.

STATE_SUCC_XXXX - indicate to the target state where it should transition to if it completes successfully. This should always be used for transitions within a controller.

STATE_ERR_XXXX - indicate to the target state where it should transition to if it completes with errors. This should always be used for transitions within a controller.

The flow of execution can be defined at any or all of the following times:

  • Controller Definition time

  • Screen Build time

  • State Run time

The preferred approach is to define the flow at Controller Definition time. This keeps the mediation logic in the controller wherever possible. Placing it in the Screen Build time allows for more flexibility while keeping the flow logic outside of the state handlers. The last option allows a state to determine where to route to based on run time considerations that are only known by the state itself (use one of the first two options whenever possible). If flow is defined in more than one of these locations then the following priority is followed:

  1. State Run

  2. Screen Build

  3. Controller Definition

Controller Definition

State default flow

When using the new features, a controller’s constructor should no longer call the addState() method. The new addStatePairing(State, State, String) and addFinalState(State) methods should be called instead.

The addStatePairing() pairs up a handle state with its associated prompt state. This will automatically cause a prompt state to be invoked if any errors were generated in the associated handle state.

The addFinalState() will be used by the Controller to identify when it needs to rerun all its handle states. This will occur automatically just prior to running the final state. This is needed in wizard screens to ensure that all data is still valid. If any errors are generated by a handle state then the associated prompt state will be invoked in order to display the error message on the appropriate screen.

The sequencing of calls to addStatePairing() is significant. This defines the order of the screens in the controller. The Controller will add 'Previous' and 'Next' transition object(s) to all prompt states’ responses. These transitions refer to either the previous prompt state (for 'Previous' buttons) or to a prompt state’s associated handle state (for 'Next' or 'Done' buttons). If no errors are generated by the handle state then the next prompt state in the sequence is then invoked.

Controller Security & Chaining flow

The following two new attributes have been added to the Controller class: controllerChainingTransition and controllerSecurityTransition. Controller Chaining allows an application to specify the controller to be invoked once the current one completes. A controller completes once the final state has run without errors. Controller Security allows an application to specify the controller to be invoked if a user encounters controller authorization failure on a state in the current controller. Both of these new attributes are of type Transition, and if needed would normally be setup in the controller’s constructor.

Screen Build

A state normally adds transition objects to its response in order to be displayed as links or buttons on an HTML page. These transition objects can have either the special parameters set or can have the enableReturnToSender() method called before doing this. Enabling the return-to-sender would cause the target controller/state to re-invoke this source state so that it can redisplay the screen to the user. Alternatively the special parameters can be used to route to some other destination.

State Run

The State class now has two new attributes of type Transition:

  • successTransition

  • errorTransition

These attributes can be set in the state’s perform() method. Doing so would override any routing that was defined at the Controller Definition or Screen Build times. When a state completes its execution, these two attributes will be used to determine which transition to execute (if any). If the ErrorCollection is not empty then the errorTransition will be used otherwise the successTransition will be executed. This approach is similar to returning an ActionForward in Struts and so state logic should now create transition objects and set the required attributes but not call the transition() method. Rather, the newly created transition objects should be assigned to one of the two new attributes of the State class.

Practice

Basically, the workflow allows you to build a sequence of screens with Previous, Next and Finish buttons. Validation and error handling management to the correct states also occurs. A special transition called enableReturnToSender() allows for a wizard to essentially call another wizard and continue where it left off.

An example of a state flow diagram:

There are three different types of states used in Expresso workflow:

  1. Prompt states: These are typically used to input user information.

  2. Handle (validate) states: These are used to validate the input entered in the prompt state.

  3. Final states: This is always the last state called in the wizard and usually is responsible for performing the final actions on the data collected in previous states.

The sequence of transitions between workflow states can be defined in three different places, namely:

  1. In the Controller (preferred): The state sequence is defined in the Controller object, externally to the states.

  2. At screen build time: The state sequence is defined at screen build time.

  3. At runtime: The state itself can decide the sequence. This is not the preferred approach as remember we are trying to build re-usable states here.

Defining the workflow in the Controller

Prompt states are paired off with handle (validate) states using the addStatePairing() function. The sequence in which the addStatePairing() function is called essentially defines the state transition sequence or routing.

For example the following code in the Controller constructor will create three wizard screens/states that can be navigated backwards and forwards from state 1 through 3.

addStatePairing(promptState1, handleState1, null); addStatePairing(promptState2, handleState2, null); addStatePairing(promptState3, handleState3, null); addFinalState(handleFinalState);

It is not possible to achieve sequences other than a simple linear sequence using a controller definition. If your wizard requires multiple paths, then you need to use either "screen build time" or "runtime" transitions.

Errors defined in the handle states will cause a return to the prompt state with appropriate error.

A special transition can be used to route all security authorization failures to a new state (for example a login state).

setControllerSecurityTransition(oneTransition);

or

setControllerChainingTransition(oneTransition);

By setting the return-to-sender it is possible to return to the original place in the first wizard once the called wizard completes.

oneTransition.enableReturnToSender(null);

Defining the workflow at screen build time

Divergent transitions can be coded into the prompt states to show additional buttons for state paths. These transitions may also be defined using the enableReturnToSender() . Obviously these transitions are built into the State code and will appear in all wizards that choose to reuse the state code, and hence are not as favorable as Controller definitions.

Defining workflow at runtime

In this case the transition is coded into the handle states and usually depends on testing of some user input. For example:

if (addDeviceForm.getSMSCarrier().equals("MTS")) { oneTransition = new Transition(); oneTransition.setState("prompt1"); oneTransition.setControllerObject("com.xyz.controller.ActivateMTS"); oneTransition.enableReturnToSender(response); setErrorTransition(oneTransition); response.addError("This message will be displayed on ActivateMTS"); }

This piece of code will cause the wizard to branch off to the com.xyz.controller.ActivateMTS controller if the form field SMScarrier in the StateForm is equal to "MTS".

Workflow sample code

This sample code uses a ControllerForm to store user input data for the session. The ControllerForm bean is defined as follows:

// // Standard form bean with getter and setter methods // This form is listed in struts-config.xml as a Struts ActionForm // and associated with the AddDevice.do action // public class AddDeviceForm extends ControllerForm { private String smsCarrier; public void setSMSCarrier(String val) { smsCarrier = val; } // This method is only called upon entry to a controller so this is // where the wizard values can all be reset public void resetController() { setSMSCarrier(null); } }

This bean has to be defined in the struts-config.xml file as follows:

<form-beans> <form-bean name="addDeviceForm" type="com.xyz.controller.AddDeviceForm"/> </form-beans>

Here's the Controller code:

// // Here the 'use-case' is AddDevice // public class AddDevice extends Controller { public AddDevice() throws NonHandleableException { super(); State promptState = null; State handleState = null; promptState = new PromptAddDevice("prompt1", "Add Device"); handleState = new ValidateAddDevice("handle1", "Add Device"); // Null in addStatePairing() indicates that a controller form // should be used addStatePairing(promptState, handleState, null); // More addStatePairing calls could go here if ever the AddDevice // function needs to split into multiple screens (ie wizard). handleState = new CompleteAddDevice("handleFinal", "Save"); addFinalState(handleState); // The next few lines will cause controller authorization failures // to route to the SignIn controller. // The return-to-sender is activated to return back to // the State that caused the authorization failure. Transition oneTransition = new Transition(); oneTransition.setState("prompt1"); oneTransition.setControllerObject("com.xyz.controller.SignIn"); try { // Will cause return to AddDevice on SignIn completed oneTransition.enableReturnToSender(null); } catch (ControllerException ce) { throw new NonHandleableException(ce); } setControllerSecurityTransition(oneTransition); // To use controller chaining call: // setControllerChainingTransition(oneTransition) } }

The controller also has to be registered in struts-config.xml as follows:

<action path="/AddDevice" type="com.xyz.controller.AddDevice" name="addDeviceForm" scope="session" validate="false"> </action>

Note that the action name refers to the form bean (ControllerForm) we defined above, and that the scope is set to 'session'. The session scope will keep the values stored in the ControllerForm intact for the duration of the session across all states. Setting scope='request' will only keep the form bean values valid across a single prompt/handle state transition.

Note also that there are no action forwards defined in our example so all views will be rendered by the default view handler.

[Authors note: It would be nice to have and example of a JSP that implements the action forwards]

Code for the sample prompt state:

public class PromptAddDevice extends State { // // The 'perform' method is overridden to provide State logic. // // The StateForm class is a new interface for the new features. // public void perform(StateForm stateForm, ControllerRequest request, ControllerResponse response) throws ControllerException, NonHandleableException { super.perform(stateForm, request, response); AddDeviceForm addDeviceForm = (AddDeviceForm) stateForm; // Add Blocks, Inputs, Outputs, Transitions here...standard stuff. // Use stateForm to populate controller elements. Input I = new Input("SMSCarrier"); Reponse.addInput(I); // This will add a link/button on the AddDevice page that will take // user to AuthorizeDevice page and when that controller completes // normally, flow returns to this prompt state again. Transition oneTransition = new Transition(); oneTransition.setState("prompt1"); oneTransition.setControllerObject("com.xyz.controller.AuthorizeDevice"); oneTransition.setName("promptAuthorizeDevice"); oneTransition.setLabel("Authorize Device"); oneTransition.enableReturnToSender(response); // To flow to another state after AuthorizeDevice is completed replace // the enableReturnToSender(response) with: // oneTransition.addParam(Controller.CTL_SUCC_CTL, // "com.xyz.controller.AnotherState"); // oneTransition.addParam(Controller.CTL_SUCC_STATE, "prompt1"); } }

Note that when populating a response with input fields that will store values in the ControllerForm. The exact name of the bean property must be used, in our case SMSCarrier . This way the default renderer will automatically fill in the ControllerForm from the Input data entered by the user.

This code also shows how to use screen time build transitions.

Code for the sample handle state:

public class ValidateAddDevice extends State { // // Validate the form. // If any errors are generated here then by default the // associated promptState will be called. This can be overridden for // certain runtime conditions/errors. // public void perform(StateForm stateForm, ControllerRequest request, ControllerResponse response) throws ControllerException, NonHandleableException { super.perform(stateForm, request, response); AddDeviceForm addDeviceForm = (AddDeviceForm) stateForm; //Example of 'State Run' routing if (addDeviceForm.getSMSCarrier().equals("MTS")) { oneTransition = new Transition(); oneTransition.setState("prompt1"); oneTransition.setControllerObject("com.xyz.controller.ActivateMTS"); oneTransition.enableReturnToSender(response); //If no msg/error is added then use setSuccessTransition(oneTransition) setErrorTransition(oneTransition); response.addError("This message will be displayed on ActivateMTS"); } else { //This will route back to prompt state so user can correct input response.addError("Invalid Carrier."); } } }

And lastly the final state

public class CompleteAddDevice extends State { public void perform(StateForm stateForm, ControllerRequest request, ControllerResponse response) throws ControllerException, NonHandleableException { super.perform(stateForm, request, response); // This is the final state so use the controller form AddDeviceForm controllerForm = (AddDeviceForm) stateForm; // Perform any database updates here } }

Conclusion

Contributors

The following persons have contributed their time to this chapter:

Note

Was this EDG documentation helpful? Do you wish to express your appreciation for the time expended over years developing the EDG doc? We now accept and appreciate monetary donations. Your support will keep the EDG doc alive and current. Please click the Donate button and enter ANY amount you think the EDG doc is worth. In appreciation of a $35+ donation, we'll give you a subscription service by emailing you notifications of doc updates; and donations $75+ will also receive an Expresso T-shirt. All online donation forms are SSL secured and payment can be made via Credit Card or your Paypal account. Thank you in advance.

Copyright © 2001-2004 Jcorporate Ltd. All rights reserved.