Hi all,

my colleague Alexander Lamm and me have implemented an extension to the core 
struts framework which provides rudimentary control flow between actions. It has 
received some considerable rework since the prior version we published and can 
now be configured much more flexible, including multiple parallel workflows.

With its help you can easily (per configuration in struts-config.xml) do the 
following:

- prevent the user from hitting the browser's reload and back buttons which can 
lead to form resubmission and data inconsistencies

- easy definitions of (what we know from non HTML GUIs as) "modal" dialogs in 
struts web applications

- centralized user authentication

- maintenance mode for the whole web application

- and some more...

The file README.TXT which I attached to this mail includes a detailed description.

Ted Husted was so kind to put the code on his Resources Page: 
[http://husted.com/about/struts/resources.htm]
 From there it can be downloaded as a .tgz file that includes all the sources 
and documentation.

One word to the workflow enhancements that are planned to be incorporated into 
Strtus 1.1: Unfortunately I did not have the time to follow the discussions on 
struts-dev as closely as I would have liked to do. So I don't know how much of 
the concepts are similar. The issue here was: We needed some simple workflow 
control for our applications and we needed it now. The planned workflow 
extensions, Craig and others are working on are still far from being finished, 
because they obviously are intended to be much more generic. So we decided to 
implement something that works for us now.

What I want to say is just this: We don't want to compete with the workflow 
extensions some other people are currently working on. If you want, you can just 
view it as an intermediate solution. And yes, I would like to see some of our 
code and concepts be reused for the workflow in Struts 1.1 but don't know if 
this makes sense.

In any case: Your feedback is very welcome.

--- Matthias
The Struts Workflow Extension
=============================

This extension is meant to provide some rudimentary framework to the
struts application framework.

The code subclasses some struts classes and provides the following
enhancements:

1. Control flow:
   For each action you can specify one or more labeled "workflows" this action
   depends on. In our notion 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.
   So one of the workflows specified for an action may be defined as "primary",
   i.e. this action belongs to the specified workflow. The others must be
   defined as "secondary", i.e. this action depends on these workflows and can
   influence their progression, but is itself not part of them.
   For each workflow you can specify the following properties:
   a) You can specify a labeled "state" this action leads to in the workflow.
        Of course you could also merely store a specific property of the action
        itself, e.g. the context-relative URI, to remember the point the
        workflow came to after this action. The abstraction we chose is a more
        appropriate description of those points though, it helps a lot in
        perceiving the progression of a workflow and it opens the faculty, that
        two actions lead to the same point in a workflow and that an action
        doesn't influence the progresion of the workflow at all.
   b) You can specify labeled states one of which must precede this action in
        the workflow. Thus, you can easily prevent someone from hitting the
        reload button or using the browser's back button and submit a form for
        the second time. The chosen mechanism can replace the token mechanism
        provided by struts and brings some further enhancements. A small
        example:
        An action "displayLogon" displays the logon page of a web application.
        It defines the workflow with the label "logon" as its primary workflow
        and leads to the state with the label "logonPage" therein. The next
        action "logonAction" defines the workflow with the label "logon"
        likewise as its primary workflow and the state "logonPage" as its only
        allowed preceding state therein. An exception is raised (and causes a
        forward to an appropriate error page), if the current state in the
        workflow "logon" is not "logonPage" when the action is called. If no
        other action can lead to this state, this means that the previous
        action must have been "displayLogon".
   c) You can specify labeled states one of which must follow directly after
        the state this action leads to in a workflow. If this property has been
        set for a workflow, no actions with a primary workflow different from
        this workflow can be executed any more. Thus, you can force someone to
        execute a series of actions which is intended to be atomic. This allows
        to introduce modal dialogues well known in a standalone application
        context into a struts web application.
   d) You can specify that the workflow ends after this action, which means
        that all stored state information is dismissed. The workflow can only
        be accessed afterwards with an action which doesn't define any
        preceding state in this workflow.

2. User authentication: For each action you can specify an object which does a
   check whether the user is authenticated to execute this action. Together
   with the paradigm (which is warmly suggested to anybody) to only display
   jsp pages through actions like displayXXX you can easily do fine grained
   authorization checks for your whole webapp.

3. Maintenance mode: If your web-site is currently under maintenance set the
   debug level to -1 and all the actions automatically forward to a maintenance
   page, which should display an appropriate message to the user.

Now, here is a brief code description. For details please have a look at the
source code which (hopefully) is richly commented.

ApplicationMapping
==================

Implementation of enhanced ActionMapping.
It defines the following custom properties to allow the specifications for an
action described above:
- primaryWorkflow:
  The label of the workflow this action mapping belongs to.
- secondaryWorkflow:
  The label of a further workflow this action mapping depends on.
- prevState:
  The label of a state which may precede this action mapping in the workflow
  given above. This property can be set multiple times to allow multiple valid
  prevStates for a workflow.
- newState:
  The label of the state this action mapping leads to in the workflow given
  above.
- nextState:
  The label of a state which may follow this action mapping in the workflow
  given above. This property can be set multiple times to allow multiple valid
  nextStates for a workflow.
- endWorkflow:
  If "true" (case insensitive string), the workflow given above ends with this
  action mapping. If this property is not given, it defaults to false.
- authtype:
  The name of the authentication class which checks if the user is allowed to
  execute the mapping's action.

For the ApplicationMapping to become effective you have to change the parameter
mapping in web.xml like this:

<init-param>
    <param-name>mapping</param-name>
    <param-value>com.livinglogic.struts.workflow.ApplicationMapping
    </param-value>
</init-param>


Workflow
========

Class that stores information concerning the current state of a labeled
workflow of this application.
It defines the following properties:
- currentState:
  The label of the state the workflow represented by this object is in at the
  moment.
- definedNextStates:
  A set of labels of the states one of which must follow the current state in
  the workflow represented by this object. If this property is set, i.e. the
  set is not null or empty, no other Workflow object than this one in the
  user's session may be accessed as the primary workflow of an action.


WorkflowContainer
================

Class that provides access by label to Workflow objects stored in a user's
session and checks the validity of returning a certain Workflow object against
all existing Workflow objects. For every user session a new instance of this
class is created.


GenericAction
=============

Extends Action and is the class from which all other Actions need to be
derived.
It does the following:

1. Check if we are in maintenance mode. If we are, then forward to the
   maintenance page.
2. Check for the authentication object (of type GenericAuthentication) which is
   specified by the action's parameter "authtype". If authentication fails,
   forward to an authentication exception action, which should display a page
   with a reasonable message.
3. For each labeled workflow:
   a) Check if the workflow is accessible at the moment. This is not the case
      if the workflow is primary for this action and different from a workflow
      that has been marked with at least one "nextState". If the check fails
      forward to a control flow exception action.
   b) Check whether one of the "prevState" mapping properties defined for this
      workflow matches the "previousState" value. The latter has been stored as
      "currentState" in a labeled workflow object in the user's session by a
      GenericAction which preceded this action in the workflow. If there is no
      match forward to a control flow exception action.
   c) Check whether the "newState" this action leads to matches one of the
      paths given in the "definedStates" value. The latter has been stored as
      "definedNextStates" in a labeled workflow object in the user's session by
      the GenericAction which directly preceded this action in the workflow. If
      it does not match forward to a control flow exception action.
4. Now call the method "performAction" (which needs to be overridden by all
   actions that subclass GenericAction). If debugging is switched off, all
   exceptions thrown in this method are catched and a forward to an exception
   action is done.
5. If no check failed and no exception occurred, update the session workflow
   object for each workflow:
   - If the "endWorkflow" attribute for this action is set to true, remove the
     workflow object from the session.
   - Else store the "newState" defined for the current action into the
     "currentState" value, if it is not null, and the set of "nextState"
     attributes defined for this workflow, which defaults to an empty set, into
     the "definedNextStates" value. So the session will hold the correct values
     when the next action in the workflow checks them.

Each check failure or exception leads to an invalidation of the user's session,
so all his session information is dismissed and he cannot go on at the point,
where he caused the exception, afterwards.


SuccessAction
=============

Always forwards to the success action path. It is used for actions which always
forward to a jsp page.


AuthenticationException
=======================

Exception thrown, when trying to access a resource that you are not allowed to
access.


GenericAuthentication
=====================

Interface that should be implemented by classes which provide authentication
checks, which are used by GenericAction.
Please note that GenericAction uses only a single instance of each
authentication object. So you have to be very careful when you use data
members.
Normally only the single method "check" is provided, which does not work on any
object's variables.


ApplicationServlet
==================

Extended version of ActionServlet.
Enables specific configuration steps at the end of the initialization
of the servlet. It defines a new init parameter for web.xml:
- initializer:
  The fully qualified Java class name of an application-specific
  class that implements the ApplicationInitializer interface and
  does all the configuration.

This init parameter in web.xml might look like this:

<init-param>
    <param-name>initializer</param-name>
    <param-value>TestInitializer</param-value>
</init-param>

For the ApplicationServlet to become effective you have to change the servlet
class in web.xml like this:

<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>com.livinglogic.struts.workflow.ApplicationServlet
    </servlet-class>
    ...
</servlet>


ApplicationInitializer
======================

Interface implemented by classes that enable initialization of all specific
resources an application needs. The init method is called at the end of the
servlet initialization. The destroy method is called before all other resources
of the servlet are destroyed.


struts-config.xml
=================

The struts configuration file can then look like this:

===============================================================================

<!-- ========== Global Forward Definitions ============================== -->
<global-forwards>
  <!-- this happens, when an authentication exception is thrown -->
  <forward name="authenticationexception"
       path="/authenticationException.jsp" />

  <!-- this happens, when we are in maintenance mode -->
  <forward name="maintenance" path="/maintenance.jsp" />

  <!-- this happens, if the workflow data do not match -->
  <forward name="controlflowexception" path="/controlFlowException.jsp"/>

  <!-- this happens, when any kind of exception occurs in one of the actions
       and debug level is > 0
  -->
  <forward name="exception" path="/exception.jsp" />
</global-forwards>

<!-- ========== Action Mapping Definitions ============================== -->
<action-mappings>

  <!-- ===== Login/logout workflow ===== -->

  <!-- Display login -->
  <action path="/displayLogin"
          type="SuccessAction">
    <set-property property="primaryWorkflow" value="login" />
    <set-property property="newState" value="1" />
    <set-property property="nextState" value="2" />
    <forward name="success" path="/index.jsp" />
  </action>

  <!-- Execute the login -->
  <action path="/loginAction"
          type="LoginAction"
          name="loginForm"
          scope="request"
          input="/index.jsp">
    <set-property property="primaryWorkflow" value="login" />
    <set-property property="prevState" value="1" />
    <set-property property="newState" value="2" />
    <forward name="success" path="/displayDataInput.do" />
  </action>

  <!-- Display logout -->
  <action path="/displayLogout"
          type="SuccessAction">
    <set-property property="primaryWorkflow" value="login" />
    <set-property property="prevState" value="2" />
    <set-property property="prevState" value="4" />
    <set-property property="newState" value="3" />
    <set-property property="nextState" value="4" />
    <set-property property="authtype" value="AnyUserAuthentication" />
    <forward name="success" path="/logout.jsp" />
  </action>

  <!-- Confirm logout -->
  <action path="/logoutAction"
          type="LogoutAction">
    <set-property property="primaryWorkflow" value="login" />
    <set-property property="prevState" value="3" />
    <set-property property="newState" value="4" />
    <set-property property="authtype" value="AnyUserAuthentication" />
    <forward name="success" path="/displayLogin.do" />
    <forward name="cancel" path="/displayDataInput.do" />
  </action>

  <!-- ===== Main data input workflow ===== -->

  <!-- Display the main data input page -->
  <action path="/displayDataInput"
          type="SuccessAction">
    <set-property property="primaryWorkflow" value="main" />
    <set-property property="newState" value="1" />
    <set-property property="authtype" value="AnyUserAuthentication" />
    <forward name="success" path="/dataInput.jsp" />
  </action>

  <!-- Save the entered data -->
  <action path="/saveDataAction"
          type="SaveDataAction">
    <set-property property="primaryWorkflow" value="main" />
    <set-property property="prevState" value="1" />
    <set-property property="newState" value="2" />
    <set-property property="authtype" value="AnyUserAuthentication" />
    <forward name="success" path="/displayDataInput.do" />
  </action>

  <!-- ===== Password change workflow ===== -->

  <!-- Display change password page -->
  <action path="/displayPasswordChange"
          type="SuccessAction">
    <set-property property="primaryWorkflow" value="password" />
    <set-property property="newState" value="1" />
    <set-property property="nextState" value="2" />
    <set-property property="secondaryWorkflow" value="main" />
    <set-property property="prevState" value="1" />
    <set-property property="authtype" value="AnyUserAuthentication" />
    <forward name="success" path="/passwordChange.jsp" />
  </action>

  <!-- Change password action for an already registered user -->
  <action path="/passwordChangeAction"
          type="PasswordChangeAction"
          name="passwordForm"
          scope="request"
          input="/passwordChange.jsp">
    <set-property property="primaryWorkflow" value="password" />
    <set-property property="prevState" value="1" />
    <set-property property="newState" value="2" />
    <set-property property="endWorkflow" value="true" />
    <set-property property="authtype" value="AnyUserAuthentication" />
    <forward name="success" path="/passwordChangeSuccess.jsp" />
  </action>

</action-mappings>

===============================================================================

See what happens?

- You are not allowed to execute displayDataInput or saveDataAction,
  displayPasswordChange or passwordChangeAction, displayLogout or logoutAction,
  when you are not correctly logged in. The framework does the check for you.
- loginAction and logoutAction are only executed, if displayLogin or
  displayLogout respectively have been executed directly before. You can only
  do a logout after a login or a cancelled logout. If you are on the login and
  logout page you may only execute the loginAction or logoutAction
  respectively, since a nextState property has been set in each case. If no
  nextState is set at the moment in a workflow, displayLogin may be called at
  any point in this application.
- saveDataAction is only executed, if displayDataInput has been executed
  directly before. If no nextState is set at the moment in a workflow,
  displayDataInput may be called at any point in this application.
- You can only display the password change page, if displayDataInput has been
  executed directly before. It is assumed that a new window is opened for the
  password change. So a new "password" workflow is opened for this window and
  the first action in this workflow depends on the "main" workflow. Furthermore
  passwordChangeAction is only executed, if displayPasswordChange has been
  executed directly before. If you are on the password change page you may only
  execute passwordChangeAction. After this action the "password" workflow is
  closed and doesn't have to be stored in the session any more.























































Reply via email to