Home
Introduction
Tutorial
Demo Application
Test Applicaton
License
Download
Commercial Support
Contributors
Struts-Homepage
Sticking to the MVC pattern

This extension relies on the assumption that your whole application strictly sticks to the Model View Controller pattern. Unfortunately the Struts framework does not really coerce you to do that. You are not forced to serve each JSP file (view) by going through a Struts action (controller). Thus, Struts allows you to leave the MVC pattern in this respect. So it is up to the developer to stick to the MVC pattern, which means that each JSP file must be delivered by an action. This action should be responsible to check whether the user is correctly logged on. Direcly accessing a JSP file should never be allowed.

On the Struts mailing list there have been quite a few discussions on this topic and the general advice is, to put all the JSP files that should not be directly accessible under the WEB-INF directory.

Each view that does not require any special tasks in its action can simply use the action mappings's forward parameter like so:

<action path="/displayLoginAction" forward="/WEB-INF/web/login.jsp" />

This definition does nothing but forward to the specified JSP page

Integration with your applications

To use this extension you must put the library struts-workflow-x-y-z.jar into your application's lib directory.

Then you must change your configuration files, so they look like this:

<struts-config>
  <form-beans>
    ...
  </form-beans>

  <global-forwards>
    ...
  </global-forwards>

  <action-mappings type="com.livinglogic.struts.workflow.WorkflowMapping">
    ...
  </action-mappings>
  
  <controller processorClass="com.livinglogic.struts.workflow.WorkflowRequestProcessor" />

</struts-config>

As you can see, you must specify the attribute type="com.livinglogic.workflow.WorkflowMapping" for all action mappings and the controller tag's attribute processorClass must be set to com.livinglogic.struts.workflow.WorkflowRequestProcessor.

From Version 1.0.2 on, the Struts Workflow Extension can also be used together with the Tiles plugin. Then you need to specify the class com.livinglogic.struts.workflow.TilesWorkflowRequestProcessor as the processor class, instead.

That should be all you need to do. Most important, you do not need to modify any of the original struts code or your application code.

Authentication checks

The Struts Framework itself only supports role based authentication checks. But as many applications implement their own session based authentication mechanism, this extension provides a simple means to control access to each action in a more flexible way. With authentication checks the test is meant, that verifies that the current user is correctly logged into the web application before executing an action that only logged in users are allowed to execute.

All you have to do is add a single property to each action definition you want to access protect:

<action path="/displayInHomeAction" forward="/WEB-INF/web/inHome.jsp">
    <set-property property="authClass" value="com.livinglogic.struts.workflow.test.TestAuthentication" />
  </action>

The class that is specified must implement the interface Authentication that defines the method check which is called upon each authentication check.

For example:

public class TestAuthentication implements Authentication
{
    public boolean check(HttpServletRequest request)
    {
        return request.getSession().getAttribute("username") != null;
    }
}

If check returns true, everything is fine, otherwise an automatic forward is done to the forward definition authenticationException which should be globally defined but can certainly be locally redefined for certain actions:

<global-forwards>
  <forward name="authenticationException" path="/WEB-INF/web/authenticationException.jsp" />
  ...
</global-forwards>

Workflow control

Now let's turn to the main feature this framework provides: Workflow control.

For each action you can specify one or more labeled "workflows" this action belongs to. In the notion of this package, workflows are logical groupings of actions following each other in a rather simple, mostly linear or circular way. More complicated sequences of actions in an application should be described by branching off new workflows at a specific point in a workflow or replacing one workflow by another. This means that several workflows are allowed to persist or progress in parallel.

One of the workflows specified for an action may be defined as "primary", the others must be defined as "secondary". An action depends on its secondary workflows and can influence their progression, but is itself not part of them.

In the Struts configuration file you can specify for each action...

  • a labeled state (newState) this action leads to in the workflow.
  • multiple labeled states one of which must precede this action in the workflow (prevState).
  • multiple labeled states one of which must follow the state this action leads to in a workflow (nextState). Specifying one or more nextState definitions lets the application developer very selectively specify, which actions are allowed to be executed directly after an action. Please note, that nextState definitions are allowed for primary workflows only. Specifying a nextState condition for a secondary workflow results in an exception, when the struts configuration file is read.
  • that the workflow ends (endWorkflow) after this action, which means that all stored state information is dismissed.

It is very important to know in which order the conditions are checked and at which point the specified action code is executed:

  1. If the previously executed action belongs to a primary workflow and has at least a nextState condition defined, the current action must change the state of this workflow to one of the allowed states (or leave the state unmodified if the workflow's state is already contained in the allowed nextState definitions). If this condition is not met, a workflow violation of the previously executed action's primary workflow is detected. If this happens, further progression is stopped and the workflow violation is handled
  2. If no workflow violation was encountered already, the prevState conditions for the current action are checked. These checks are performed in the order the workflows are specified in the configuration, i. e. whether a workflow for the current action is a primary or a secondary one does not matter concerning the order of prevState checks. If the workflow that is currently checked defines at least a prevState, a workflow with the same label must already be active and the state this workflow is in, must be one of the prevState values that are specified. Otherwise, a workflow violation for this workflow is encountered and further processing is stopped.
  3. If no workflow violation is encountered, all workflows' state information is updated, i. e. the states of all defined workflows are set to the specified new values and all the workflows that end with the execution of this action are cleaned up and removed.
  4. After having updated all workflows, the action code is executed. Please note that all workflows have already been updated before. Remember in particular, that every workflow that is specified to end with this action has already been removed at the time of action code execution.
If a workflow is violated, it is cleaned up and removed from the current session and the action code is not executed. Instead, control is forwarded to a global forward definition workflowViolation_workflowLabel with workflowLabel specifying the name of the workflow that was violated.

Here is an example action definition that illustrates the above:

<action path="/beginWf3Transition"
        type="com.livinglogic.struts.workflow.test.Wf3St1Action">
  <set-property property="authClass" value="com.livinglogic.struts.workflow.test.TestAuthentication" />
  <set-property property="secondaryWorkflow" value="wf2" />
  <set-property property="prevState" value="suspended" />
  <set-property property="prevState" value="2" />
  <set-property property="newState" value="suspended" />
  <set-property property="primaryWorkflow" value="wf3" />
  <set-property property="newState" value="1" />
  <set-property property="nextState" value="2" />
  <forward name="success" path="/WEB-INF/web/inHome.jsp" />
</action>

This action can only be executed, if a workflow with the label wf2 is active and currently in the state suspended or 2. If someone tries to execute this action, even though this is not true, the action will not be executed. Instead control is forwarded to the global forward definition workflowViolation_wf2. The action that is associated with this forward can again violate a workflow, if its prevState conditions are not met.

If the above action's prevState condition is met, the action brings this workflow into the state suspended and the workflow wf3 (which is the primary workflow, i. e. the workflow this action belongs to) into the state 1. It also defines a nextState condition for this workflow, which means that the action that is requested after this action must bring this workflow into the state 2. If the user tries to execute an action that does not do that, the workflow wf3 is violated, i. e. control is forwarded to workflowViolation_wf3.

The nextState condition is a very powerful condition that allows the developer to define modal dialogs, because it denies the user from executing another action than one of the actions that meet the defined nextState condition. However in every web application, there are actions that must be executable no matter in which workflow state the application currently is. For instance, you can think of an application that always wants to provide a "Logout"-Button. What if the user is in the middle of a modal dialog workflow, i. e. the action for displaying the dialog page has been executed already and the nextState-condition expects the next action to bring this workflow into a certain state? There is a simple means for cases like this one. An action can be excluded from all workflow checks by setting the property noWorkflowChecks to true in the action mapping like so:

<action path="/logoutAction"
        type="com.livinglogic.struts.workflow.test.LogoutAction">
  <set-property property="noWorkflowChecks" value="true" />
  <forward name="success" path="/displayLoginAction.do" />
</action>

If this property is set, no primary or secondary workflow definitions are allowed for the same action mapping. When the action is executed, no nextState conditions of the previous primary workflow are checked and all the active workflows are left untouched, i. e. after the action has been executed, all the workflows are in exactly the same states like if the action had not been executed at all. In our example this would normally mean that after the logout action has been executed, the next action would need to bring the workflow of the modal dialog into the state that was defined by the nextState condition in order to be a valid action. I said normally, because in our special case, the action invalidates the user's session that stores all workflow state information. Thus, after the action has been executed, no workflows are active any longer and the application behaves like if the user newly visits the application.

Workflow scope

In every "normal" Java-based web application, there are at least three different scopes available: application, session and request scope. Each of these scopes allows the web application developer to store attributes. In order to keep user sessions lean and clean, it is desirable to use the request scope whenever possible over session scope. However, as soon as a data object that cannot be passed around with hidden fields needs to be available across multiple requests, this object must to be put in session scope.

This can turn into a problem in some cases, as in a normal web application, the user can enter a sequence of actions, leave this sequence and turn to some other part of the application. The session scope attributes that have been stored in the sequence of actions, the user accessed before simply stay there until the session times out or the user enters the same sequence of actions again and finishes it, so the session is cleaned up with the last action of this sequence. Therefore, what we would like to have is a more granular scope that persists longer than the request scope but not for the whole session. This is what the workflow scope does. This scope can be used to store attributes just like the session scope, but has one significant difference: There is a separate scope for each workflow, which is cleaned up when a workflow ends. The following code snippet demonstrates how the workflow scope can be accessed from within an action:

Workflow workflow = WorkflowContainer.get(session).getPrimary();
if (null != workflow)
{
  workflow.setAttribute("anAttribute", "Example Attribute");
}

Only the primary workflow's scope can be accessed from an action. If the action does not belong to a primary workflow, the method getPrimary returns null. Please note, thate the workflow state information is always updated before the action code is executed. Thus, in an action that ends its primary workflow, the method getPrimary returns null, because at the time it is called, the workflow has already been cleaned up and removed.

In many cases, the workflow scope is not sufficient for attribute storage. Often enough it is necessary to store attributes in the session scope, e.g. because multiple different workflows need to have access to it. These session scope attributes are not automatically cleaned up during workflow cleanup. For cases like this, the Struts Workflow Extension offers the application developer the chance to add cleanup objects to a workflow. A cleanup object is a class that must implement the interface WorkflowCleanup which defines the method cleanup(HttpServletRequest). Here is an example class that cleans the user's session from the session variable wizardForm, when the cleanup method is executed.

public static class WizardFormCleanup implements
  WorkflowCleanup
{
    public void cleanup(HttpServletRequest request)
    {
        request.getSession().removeAttribute("wizardForm");
    }
}

This is how this cleanup object can be added to the primary workflow of an action:

Workflow workflow =
  WorkflowContainer.get(session).getPrimary();
if (null != workflow)
{
  workflow.addCleanupObject("wizardFormCleanup",
    new WizardFormCleanup());
}

When a workflow ends, because it was finished normally or it was violated, all cleanup objects' cleanup methods are executed in the reverse order as they were added, i. e. the cleanup object added last, will be executed first. Again, the cleanup tasks are performed before the corresponding action code

For further documentation on all these concepts refer to the example application that is provided with this package.