You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC

svn commit: r627255 [39/41] - in /incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/ com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/ com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki...

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Decision.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Decision.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Decision.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Decision.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,239 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2007 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.ecyrd.jspwiki.WikiException;
+
+/**
+ * <p>
+ * AbstractStep subclass that asks an actor Principal to choose an Outcome on
+ * behalf of an owner (also a Principal). The actor "makes the decision" by
+ * calling the {@link #decide(Outcome)} method. When this method is called,
+ * it will set the Decision's Outcome to the one supplied. If the parent
+ * Workflow is in the {@link Workflow#WAITING} state, it will be re-started.
+ * Any checked WikiExceptions thrown by the workflow after re-start will be
+ * re-thrown to callers.
+ * </p>
+ * <p>
+ * When a Decision completes, its
+ * {@link #isCompleted()} method returns <code>true</code>. It also tells its
+ * parent WorkflowManager to remove it from the list of pending tasks by calling
+ * {@link DecisionQueue#remove(Decision)}.
+ * </p>
+ * <p>
+ * To enable actors to choose an appropriate Outcome, Decisions can store
+ * arbitrary key-value pairs called "facts." These facts can be presented by the
+ * user interface to show details the actor needs to know about. Facts are added
+ * by calling classes to the Decision, in order of expected presentation, by the
+ * {@link #addFact(Fact)} method. They can be retrieved, in order, via {@link #getFacts()}.
+ * </p>
+ *
+ * @author Andrew Jaquith
+ * @since 2.5
+ */
+public abstract class Decision extends AbstractStep
+{
+    private Principal m_actor;
+
+    private int m_id;
+
+    private final Outcome m_defaultOutcome;
+
+    private final List<Fact> m_facts;
+
+    /**
+     * Constructs a new Decision for a required "actor" Principal, having a default Outcome.
+     * @param workflow the parent Workflow object
+     * @param messageKey the i18n message key that represents the message the actor will see
+     * @param actor the Principal (<em>e.g.</em>, a WikiPrincipal, Role, GroupPrincipal) who is
+     * required to select an appropriate Outcome
+     * @param defaultOutcome the Outcome that the user interface will recommend as the
+     * default choice
+     */
+    public Decision(Workflow workflow, String messageKey, Principal actor, Outcome defaultOutcome)
+    {
+        super(workflow, messageKey);
+        m_actor = actor;
+        m_defaultOutcome = defaultOutcome;
+        m_facts = new ArrayList<Fact>();
+        addSuccessor(defaultOutcome, null);
+    }
+
+    /**
+     * Appends a Fact to the list of Facts associated with this Decision.
+     *
+     * @param fact
+     *            the new fact to add
+     */
+    public final void addFact(Fact fact)
+    {
+        m_facts.add(fact);
+    }
+
+    /**
+     * <p>Sets this Decision's outcome, and restarts the parent Workflow if
+     * it is in the {@link Workflow#WAITING} state and this Decision is
+     * its currently active Step. Any checked WikiExceptions thrown by
+     * the workflow after re-start will be re-thrown to callers.</p>
+     * <p>This method cannot be invoked if the Decision is not the
+     * current Workflow step; all other invocations will throw
+     * an IllegalStateException. If the Outcome supplied to this method
+     * is one one of the Outcomes returned by {@link #getAvailableOutcomes()},
+     * an IllegalArgumentException will be thrown.</p>
+     *
+     * @param outcome
+     *            the Outcome of the Decision
+     * @throws WikiException
+     *             if the act of restarting the Workflow throws an exception
+     */
+    public void decide(Outcome outcome) throws WikiException
+    {
+        super.setOutcome(outcome);
+
+        // If current workflow is waiting for input, restart it and remove
+        // Decision from DecisionQueue
+        Workflow w = getWorkflow();
+        if (w.getCurrentState() == Workflow.WAITING && this.equals(w.getCurrentStep()))
+        {
+            WorkflowManager wm = w.getWorkflowManager();
+            if (wm != null)
+            {
+                wm.getDecisionQueue().remove(this);
+            }
+            // Restart workflow
+            w.restart();
+        }
+    }
+
+    /**
+     * Default implementation that always returns {@link Outcome#STEP_CONTINUE}
+     * if the current Outcome isn't a completion (which will be true if the
+     * {@link #decide(Outcome)} method hasn't been executed yet. This method
+     * will also add the Decision to the associated DecisionQueue.
+     * @return the Outcome of the execution
+     * @throws WikiException never
+     */
+    public Outcome execute() throws WikiException
+    {
+        if (getOutcome().isCompletion())
+        {
+            return getOutcome();
+        }
+
+        // Put decision in the DecisionQueue
+        WorkflowManager wm = getWorkflow().getWorkflowManager();
+        if (wm != null)
+        {
+            wm.getDecisionQueue().add(this);
+        }
+
+        // Indicate we are waiting for user input
+        return Outcome.STEP_CONTINUE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public final Principal getActor()
+    {
+        return m_actor;
+    }
+
+    /**
+     * Returns the default or suggested outcome, which must be one of those
+     * returned by {@link #getAvailableOutcomes()}. This method is guaranteed
+     * to return a non-<code>null</code> Outcome.
+     *
+     * @return the default outcome.
+     */
+    public Outcome getDefaultOutcome()
+    {
+        return m_defaultOutcome;
+    }
+
+    /**
+     * Returns the Facts associated with this Decision, in the order in which
+     * they were added.
+     *
+     * @return the list of Facts
+     */
+    public final List<Fact> getFacts()
+    {
+        return Collections.unmodifiableList(m_facts);
+    }
+
+    /**
+     * Returns the unique identifier for this Decision. Normally, this ID is
+     * programmatically assigned when the Decision is added to the
+     * DecisionQueue.
+     *
+     * @return the identifier
+     */
+    public final int getId()
+    {
+        return m_id;
+    }
+
+    /**
+     * Returns <code>true</code> if the Decision can be reassigned to another
+     * actor. This implementation always returns <code>true</code>.
+     *
+     * @return the result
+     */
+    public boolean isReassignable()
+    {
+        return true;
+    }
+
+    /**
+     * Reassigns the Decision to a new actor (that is, provide an outcome).
+     * If the Decision is not reassignable, this method throws 
+     * an IllegalArgumentException.
+     *
+     * @param actor the actor to reassign the Decision to
+     */
+    public final synchronized void reassign(Principal actor)
+    {
+        if (isReassignable())
+        {
+            m_actor = actor;
+        }
+        else
+        {
+            throw new IllegalArgumentException("Decision cannot be reassigned.");
+        }
+    }
+
+    /**
+     * Sets the unique identfier for this Decision.
+     *
+     * @param id
+     *            the identifier
+     */
+    public final void setId(int id)
+    {
+        m_id = id;
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionQueue.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionQueue.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionQueue.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionQueue.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,185 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2007 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+
+import com.ecyrd.jspwiki.WikiException;
+import com.ecyrd.jspwiki.WikiSession;
+
+/**
+ * Keeps a queue of pending Decisions that need to be acted on by named
+ * Principals.
+ *
+ * @author Andrew Jaquith
+ * @since 2.5
+ */
+public class DecisionQueue
+{
+
+    private LinkedList<Decision> m_queue = new LinkedList<Decision>();
+
+    private volatile int m_next;
+
+    /**
+     * Constructs a new DecisionQueue.
+     */
+    public DecisionQueue()
+    {
+        m_next = 1000;
+    }
+
+    /**
+     * Adds a Decision to the DecisionQueue; also sets the Decision's unique
+     * identifier.
+     *
+     * @param decision
+     *            the Decision to add
+     */
+    protected synchronized void add(Decision decision)
+    {
+        m_queue.addLast(decision);
+        decision.setId(nextId());
+    }
+
+    /**
+     * Protected method that returns all pending Decisions in the queue, in
+     * order of submission. If no Decisions are pending, this method returns a
+     * zero-length array.
+     *
+     * @return the pending decisions TODO: explore whether this method could be
+     *         made protected
+     */
+    protected Decision[] decisions()
+    {
+        return m_queue.toArray(new Decision[m_queue.size()]);
+    }
+
+    /**
+     * Protected method that removes a Decision from the queue.
+     * @param decision the decision to remove
+     */
+    protected synchronized void remove(Decision decision)
+    {
+        m_queue.remove(decision);
+    }
+
+    /**
+     * Returns a Collection representing the current Decisions that pertain to a
+     * users's WikiSession. The Decisions are obtained by iterating through the
+     * WikiSession's Principals and selecting those Decisions whose
+     * {@link Decision#getActor()} value match. If the wiki session is not
+     * authenticated, this method returns an empty Collection.
+     *
+     * @param session
+     *            the wiki session
+     * @return the collection of Decisions, which may be empty
+     */
+    public Collection<Decision> getActorDecisions(WikiSession session)
+    {
+        ArrayList<Decision> decisions = new ArrayList<Decision>();
+        if (session.isAuthenticated())
+        {
+            Principal[] principals = session.getPrincipals();
+            Principal[] rolePrincipals = session.getRoles();
+            for ( Decision decision : m_queue )
+            {
+                // Iterate through the Principal set
+                for (int i = 0; i < principals.length; i++)
+                {
+                    if (principals[i].equals(decision.getActor()))
+                    {
+                        decisions.add(decision);
+                    }
+                }
+                // Iterate through the Role set
+                for (int i = 0; i < rolePrincipals.length; i++)
+                {
+                    if (rolePrincipals[i].equals(decision.getActor()))
+                    {
+                        decisions.add(decision);
+                    }
+                }
+            }
+        }
+        return decisions;
+    }
+
+    /**
+     * Attempts to complete a Decision by calling
+     * {@link Decision#decide(Outcome)}. This will cause the Step immediately
+     * following the Decision (if any) to start. If the decision completes
+     * successfully, this method also removes the completed decision from the
+     * queue.
+     *
+     * @param decision the Decision for which the Outcome will be supplied
+     * @param outcome the Outcome of the Decision
+     * @throws WikiException if the succeeding Step cannot start
+     * for any reason
+     */
+    public void decide(Decision decision, Outcome outcome) throws WikiException
+    {
+        decision.decide(outcome);
+        if (decision.isCompleted())
+        {
+            remove(decision);
+        }
+
+        // TODO: We should fire an event indicating the Outcome, and whether the
+        // Decision completed successfully
+    }
+
+    /**
+     * Reassigns the owner of the Decision to a new owner. Under the covers,
+     * this method calls {@link Decision#reassign(Principal)}.
+     *
+     * @param decision the Decision to reassign
+     * @param owner the new owner
+     * @throws WikiException never
+     */
+    public synchronized void reassign(Decision decision, Principal owner) throws WikiException
+    {
+        if (decision.isReassignable())
+        {
+            decision.reassign(owner);
+
+            // TODO: We should fire an event indicating the reassignment
+            return;
+        }
+        throw new IllegalStateException("Reassignments not allowed for this decision.");
+    }
+
+    /**
+     * Returns the next available unique identifier, which is subsequently
+     * incremented.
+     *
+     * @return the id
+     */
+    private synchronized int nextId()
+    {
+        int current = m_next;
+        m_next++;
+        return current;
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionRequiredException.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionRequiredException.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionRequiredException.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/DecisionRequiredException.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,46 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2004 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import com.ecyrd.jspwiki.WikiException;
+
+/**
+ * Exception thrown when an activity -- that would otherwise complete silently --
+ * cannot complete because a workflow {@link Decision} is required. The message
+ * string should be a human-readable, internationalized String explaining why
+ * the activity could not complete, or that the activity has been queued for 
+ * review.
+ *
+ * @author Andrew Jaquith
+ */
+public class DecisionRequiredException extends WikiException
+{
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception.
+     * @param message the message
+     */
+    public DecisionRequiredException(String message)
+    {
+        super(message);
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Fact.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Fact.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Fact.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Fact.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,106 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+/**
+ * Represents a contextual artifact, which can be any Object, useful for making
+ * a Decision. Facts are key-value pairs, where the key is a String (message
+ * key) and the value is an arbitrary Object. Generally, the supplied object's
+ * {@link #toString()} method should return a human-readable String. Facts are
+ * immutable objects.
+ * 
+ * @author Andrew Jaquith
+ * @since 2.5
+ */
+public final class Fact
+{
+    private final String m_key;
+
+    private final Object m_obj;
+
+    /**
+     * Constructs a new Fact with a supplied message key and value.
+     * 
+     * @param messageKey
+     *            the "name" of this fact, which should be an i18n message key
+     * @param value
+     *            the object to associate with the name
+     */
+    public Fact(String messageKey, Object value)
+    {
+        if ( messageKey == null || value == null )
+        {
+            throw new IllegalArgumentException( "Fact message key or value parameters must not be null." );
+        }
+        m_key = messageKey;
+        m_obj = value;
+    }
+
+    /**
+     * Returns this Fact's name, as represented an i18n message key.
+     * @return the message key
+     */
+    public String getMessageKey()
+    {
+        return m_key;
+    }
+
+    /**
+     * Returns this Fact's value.
+     * @return the value object
+     */
+    public Object getValue()
+    {
+        return m_obj;
+    }
+    
+    /**
+     * Two Facts are considered equal if their message keys and value objects are equal.
+     * @param obj the object to test
+     * @return <code>true</code> if logically equal, <code>false</code> if not
+     */
+    public boolean equals( Object obj )
+    {
+        if ( !( obj instanceof Fact ) ) 
+        {
+            return false;
+        }
+        
+        Fact f = (Fact)obj;
+        return m_key.equals( f.m_key) && m_obj.equals( f.m_obj );
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public int hashCode()
+    {
+        return m_key.hashCode() + 41 * m_obj.hashCode();
+    }
+
+    /**
+     * Returns a String representation of this Fact.
+     * @return the representation
+     */
+    public String toString()
+    {
+        return "[Fact:" + m_obj.toString() + "]";
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/NoSuchOutcomeException.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/NoSuchOutcomeException.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/NoSuchOutcomeException.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/NoSuchOutcomeException.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,41 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+/**
+ * Exception thrown when an attempt is made to find an Outcome that does not
+ * exist.
+ *
+ * @author Andrew Jaquith
+ */
+public class NoSuchOutcomeException extends Exception
+{
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception.
+     * @param message the message
+     */
+    public NoSuchOutcomeException(String message)
+    {
+        super(message);
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Outcome.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Outcome.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Outcome.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Outcome.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,163 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+/**
+ * Resolution of a workflow Step, such as "approve," "deny," "hold," "task
+ * error," or other potential resolutions.
+ *
+ * @author Andrew Jaquith
+ * @since 2.5
+ */
+public final class Outcome
+{
+
+    /** Complete workflow step (without errors) */
+    public static final Outcome STEP_COMPLETE = new Outcome("outcome.step.complete", true);
+
+    /** Terminate workflow step (without errors) */
+    public static final Outcome STEP_ABORT = new Outcome("outcome.step.abort", true);
+
+    /** Continue workflow step (without errors) */
+    public static final Outcome STEP_CONTINUE = new Outcome("outcome.step.continue", false);
+
+    /** Acknowlege the Decision. */
+    public static final Outcome DECISION_ACKNOWLEDGE = new Outcome("outcome.decision.acknowledge", true);
+
+    /** Approve the Decision (and complete the step). */
+    public static final Outcome DECISION_APPROVE = new Outcome("outcome.decision.approve", true);
+
+    /** Deny the Decision (and complete the step). */
+    public static final Outcome DECISION_DENY = new Outcome("outcome.decision.deny", true);
+
+    /** Put the Decision on hold (and pause the step). */
+    public static final Outcome DECISION_HOLD = new Outcome("outcome.decision.hold", false);
+
+    /** Reassign the Decision to another actor (and pause the step). */
+    public static final Outcome DECISION_REASSIGN = new Outcome("outcome.decision.reassign", false);
+
+    private static final Outcome[] OUTCOMES = new Outcome[] { STEP_COMPLETE, STEP_ABORT, STEP_CONTINUE, DECISION_ACKNOWLEDGE,
+                                                               DECISION_APPROVE, DECISION_DENY, DECISION_HOLD, DECISION_REASSIGN };
+
+    private final String m_key;
+
+    private final boolean m_completion;
+
+    /**
+     * Private constructor to prevent direct instantiation.
+     *
+     * @param key
+     *            message key for the Outcome
+     * @param completion
+     *            whether this Outcome should be interpreted as the logical
+     *            completion of a Step.
+     */
+    private Outcome(String key, boolean completion)
+    {
+        if (key == null)
+        {
+            throw new IllegalArgumentException("Key cannot be null.");
+        }
+        m_key = key;
+        m_completion = completion;
+    }
+
+    /**
+     * Returns <code>true</code> if this Outcome represents a completion
+     * condition for a Step.
+     *
+     * @return the result
+     */
+    public boolean isCompletion()
+    {
+        return m_completion;
+    }
+
+    /**
+     * The i18n key for this outcome, which is prefixed by <code>outcome.</code>.
+     * If calling classes wish to return a locale-specific name for this task
+     * (such as "approve this request"), they can use this method to obtain the
+     * correct key suffix.
+     *
+     * @return the i18n key for this outcome
+     */
+    public String getMessageKey()
+    {
+        return m_key;
+    }
+
+    /**
+     * The hashcode of an Outcome is identical to the hashcode of its message
+     * key, multiplied by 2 if it is a "completion" Outcome.
+     * @return the hash code
+     */
+    public int hashCode()
+    {
+        return m_key.hashCode() * (m_completion ? 1 : 2);
+    }
+
+    /**
+     * Two Outcome objects are equal if their message keys are equal.
+     * @param obj the object to test
+     * @return <code>true</code> if logically equal, <code>false</code> if not
+     */
+    public boolean equals(Object obj)
+    {
+        if (!(obj instanceof Outcome))
+        {
+            return false;
+        }
+        return m_key.equals(((Outcome) obj).getMessageKey());
+    }
+
+    /**
+     * Returns a named Outcome. If an Outcome matching the supplied key is not
+     * found, this method throws a {@link NoSuchOutcomeException}.
+     *
+     * @param key
+     *            the name of the outcome
+     * @return the Outcome
+     * @throws NoSuchOutcomeException
+     *             if an Outcome matching the key isn't found.
+     */
+    public static Outcome forName(String key) throws NoSuchOutcomeException
+    {
+        if (key != null)
+        {
+            for (int i = 0; i < OUTCOMES.length; i++)
+            {
+                if (OUTCOMES[i].m_key.equals(key))
+                {
+                    return OUTCOMES[i];
+                }
+            }
+        }
+        throw new NoSuchOutcomeException("Outcome " + key + " not found.");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String toString()
+    {
+        return "[Outcome:" + m_key + "]";
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleDecision.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleDecision.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleDecision.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleDecision.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,51 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+
+/**
+ * Decision subclass that includes two available Outcomes:
+ * {@link Outcome#DECISION_APPROVE} or {@link Outcome#DECISION_DENY}.
+ * The Decision is reassignable, and the default Outcome is 
+ * {@link Outcome#DECISION_APPROVE}.
+ * 
+ * @author Andrew Jaquith
+ */
+public class SimpleDecision extends Decision
+{
+
+    /**
+     * Constructs a new SimpleDecision assigned to a specified actor.
+     * @param workflow the parent Workflow
+     * @param messageKey the message key that describes the Decision, which
+     * will be presented in the UI
+     * @param actor the Principal (<em>e.g.</em>, WikiPrincipal,
+     * GroupPrincipal, Role) who will decide
+     */
+    public SimpleDecision(Workflow workflow, String messageKey, Principal actor)
+    {
+        super(workflow, messageKey, actor, Outcome.DECISION_APPROVE);
+
+        // Add the other default outcomes
+        super.addSuccessor(Outcome.DECISION_DENY, null);
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleNotification.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleNotification.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleNotification.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SimpleNotification.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,77 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+
+import com.ecyrd.jspwiki.WikiException;
+
+/**
+ * Decision subclass used for notifications that includes only one available Outcome:
+ * {@link Outcome#DECISION_ACKNOWLEDGE}. The Decision is not reassignable, and
+ * the default Outcome is {@link Outcome#DECISION_ACKNOWLEDGE}.
+ * 
+ * @author Andrew Jaquith
+ * @since 2.5
+ */
+public final class SimpleNotification extends Decision
+{
+
+    /**
+     * Constructs a new SimpleNotification object with a supplied message key,
+     * associated Workflow, and named actor who must acknowledge the message.
+     * The notification is placed in the Principal's list of queued Decisions.
+     * Because the only available Outcome is
+     * {@link Outcome#DECISION_ACKNOWLEDGE}, the actor can only acknowledge the
+     * message.
+     * 
+     * @param workflow
+     *            the Workflow to associate this notification with
+     * @param messageKey
+     *            the message key
+     * @param actor
+     *            the Principal who will acknowledge the message
+     */
+    public SimpleNotification(Workflow workflow, String messageKey, Principal actor)
+    {
+        super(workflow, messageKey, actor, Outcome.DECISION_ACKNOWLEDGE);
+    }
+    
+    /**
+     * Convenience method that simply calls {@link #decide(Outcome)} 
+     * with the value {@link Outcome#DECISION_ACKNOWLEDGE}.
+     * @throws WikiException never
+     */
+    public void acknowledge() throws WikiException
+    {
+        this.decide( Outcome.DECISION_ACKNOWLEDGE );
+    }
+
+    /**
+     * Notifications cannot be re-assigned, so this method always returns
+     * <code>false</code>.
+     * @return <code>false</code> always
+     */
+    public final boolean isReassignable()
+    {
+        return false;
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Step.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Step.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Step.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Step.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,246 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+import com.ecyrd.jspwiki.WikiException;
+
+/**
+ * <p>
+ * Discrete unit of work in a Workflow, such as a {@link Decision} or a
+ * {@link Task}. Decisions require user input, while Tasks do not. All Steps,
+ * however, possess these properties:
+ * </p>
+ * <ul>
+ * <li><strong>actor</strong>: the Principal responsible for executing the
+ * Step; returned by {@link Step#getActor()}.</li>
+ * <li><strong>availableOutcomes</strong>: a collection of possible
+ * "outcomes," such as "approve decision" ({@link Outcome#DECISION_APPROVE}),
+ * "reassign decision" ({@link Outcome#DECISION_REASSIGN}), "abort step" ({@link Outcome#STEP_ABORT})
+ * and others. The range of possible Outcomes for the Step is returned by
+ * {@link Step#getAvailableOutcomes()}; see the Outcome class for more details.</li>
+ * <li><strong>errors</strong>: an collection of Strings indicating errors
+ * returned by the Step. These values are returned by {@link Step#getErrors()}.</li>
+ * <li><strong>started</strong> and <strong>completed</strong>: whether the
+ * Step has started/finished. These values are returned by
+ * {@link Step#isStarted()} and {@link Step#isCompleted()}.</li>
+ * <li><strong>startTime</strong> and <strong>endTime</strong>: the time when
+ * the Step started and finished. These values are returned by
+ * {@link Step#getStartTime()} and {@link Step#getEndTime()}, respectively.</li>
+ * <li><strong>workflow</strong>: the parent Workflow. </li>
+ * </ul>
+ * <p>
+ * Steps contain a {@link #getMessageKey()} method that returns a key that can
+ * be used with the {@link com.ecyrd.jspwiki.i18n.InternationalizationManager}.
+ * See also {@link Workflow#getMessageArguments()}, which is a convenience
+ * method that returns message arguments.
+ * </p>
+ * 
+ * @author Andrew Jaquith
+ * @since 2.5
+ */
+public interface Step
+{
+
+    /**
+     * Adds a successor Step to this one, which will be triggered by a supplied
+     * Outcome. Implementations should respect the order in which Outcomes are
+     * added; {@link #getAvailableOutcomes()} should return them in the same
+     * order they were added.
+     * 
+     * @param outcome
+     *            the Outcome triggering a particular successor Step
+     * @param step
+     *            the Step to associated with this Outcomes (<code>null</code>
+     *            denotes no Steps)
+     */
+    public void addSuccessor(Outcome outcome, Step step);
+
+    /**
+     * Returns a Collection of available outcomes, such as "approve", "deny" or
+     * "reassign", in the order in which they were added via
+     * {@link #addSuccessor(Outcome, Step)}. Concrete implementations should
+     * always return a defensive copy of the outcomes, not the original backing
+     * collection.
+     * 
+     * @return the set of outcomes
+     */
+    public Collection getAvailableOutcomes();
+
+    /**
+     * Returns a List of error strings generated by this Step. If this Step
+     * generated no errors, this method returns a zero-length array.
+     * 
+     * @return the errors
+     */
+    public List getErrors();
+
+    /**
+     * <p>
+     * Executes the processing for this Step and returns an Outcome indicating
+     * if it succeeded ({@link Outcome#STEP_COMPLETE} or
+     * {@link Outcome#STEP_ABORT}). Processing instructions can do just about
+     * anything, such as executing custom business logic or changing the Step's
+     * final outcome via {@link #setOutcome(Outcome)}. A return value of
+     * <code>STEP_COMPLETE</code> indicates that the instructions executed completely,
+     * without errors; <code>STEP_ABORT</code> indicates that the Step and its
+     * parent Workflow should be aborted (that is, fail silently without error).
+     * If the execution step encounters any errors, it should throw a
+     * WikiException or a subclass.
+     * </p>
+     * <p>
+     * Note that successful execution of this methods does not necessarily mean
+     * that the Step is considered "complete"; rather, it just means that it has
+     * executed. Therefore, it is possible that <code>execute</code> could run
+     * multiple times.
+     * </p>
+     * 
+     * @return the result of the Step, where <code>true</code> means success,
+     *         and <code>false</code> means abort
+     * @throws WikiException
+     *             if the step encounters errors while executing
+     */
+    public Outcome execute() throws WikiException;
+
+    /**
+     * The Principal responsible for completing this Step, such as a system user
+     * or actor assigned to a Decision.
+     * 
+     * @return the responsible Principal
+     */
+    public Principal getActor();
+
+    /**
+     * The end time for this Step. This value should be set when the step
+     * completes. Returns {@link Workflow#TIME_NOT_SET} if not completed yet.
+     * 
+     * @return the end time
+     */
+    public Date getEndTime();
+
+    /**
+     * Message key for human-friendly name of this Step, including any parameter
+     * substitutions. By convention, the message prefix should be a lower-case
+     * version of the Step's type, plus a period (<em>e.g.</em>,
+     * <code>task.</code> and <code>decision.</code>).
+     * 
+     * @return the message key for this Step.
+     */
+    public String getMessageKey();
+
+    /**
+     * Returns the message arguments for this Step, typically by delegating to the
+     * parent Workflow's {@link Workflow#getMessageArguments()} method.
+     * 
+     * @return the message arguments.
+     */
+    public Object[] getMessageArguments();
+
+    /**
+     * Returns the Outcome of this Step's processing; by default,
+     * {@link Outcome#STEP_CONTINUE}.
+     * 
+     * @return the outcome
+     */
+    public Outcome getOutcome();
+
+    /**
+     * The start time for this Step. Returns {@link Workflow#TIME_NOT_SET} if
+     * not started yet.
+     * 
+     * @return the start time
+     */
+    public Date getStartTime();
+
+    /**
+     * Gets the Workflow that is the parent of this Step.
+     * 
+     * @return the workflow
+     */
+    public Workflow getWorkflow();
+
+    /**
+     * Determines whether the Step is completed; if not, it is by definition
+     * awaiting action by the owner or in process. If a Step has completed, it
+     * <em>must also</em> return a non-<code>null</code> result for
+     * {@link #getOutcome()}.
+     * 
+     * @return <code>true</code> if the Step has completed; <code>false</code>
+     *         if not.
+     */
+    public boolean isCompleted();
+
+    /**
+     * Determines whether the Step has started.
+     * 
+     * @return <code>true</code> if the Step has started; <code>false</code>
+     *         if not.
+     */
+    public boolean isStarted();
+
+    /**
+     * Starts the Step, and sets the start time to the moment when this method
+     * is first invoked. If this Step has already been started, this method
+     * throws an {@linkplain IllegalStateException}. If the Step cannot
+     * be started because the underlying implementation encounters an error,
+     * it the implementation should throw a WikiException.
+     * 
+     * @throws WikiException if the step encounters errors while starting
+     */
+    public void start() throws WikiException;
+
+    /**
+     * Sets the current Outcome for the step. If the Outcome is a "completion"
+     * Outcome, it should also sets the completon time and mark the Step as
+     * complete. Once a Step has been marked complete, this method cannot be
+     * called again. If the supplied Outcome is not in the set returned by
+     * {@link #getAvailableOutcomes()}, or is not  {@link Outcome#STEP_CONTINUE}
+     * or {@link Outcome#STEP_ABORT}, this method returns an
+     * IllegalArgumentException. If the caller attempts to set an Outcome
+     * and the Step has already completed, this method throws an 
+     * IllegalStateException.
+     * 
+     * @param outcome whether the step should be considered completed
+     */
+    public void setOutcome(Outcome outcome);
+
+    /**
+     * Convenience method that returns the owner of the Workflow by delegating
+     * to {@link Workflow#getOwner()}.
+     * 
+     * @return the owner of the Workflow
+     */
+    public Principal getOwner();
+
+    /**
+     * Identifies the next Step for a particular Outcome; if there is no next
+     * Step for this Outcome, this method returns <code>null</code>.
+     * 
+     * @param outcome
+     *            the outcome
+     * @return the next step
+     */
+    public Step getSuccessor(Outcome outcome);
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SystemPrincipal.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SystemPrincipal.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SystemPrincipal.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/SystemPrincipal.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,53 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+
+/**
+ * System users asociated with workflow Task steps.
+ * 
+ * @author Andrew Jaquith
+ */
+public final class SystemPrincipal implements Principal
+{
+    /** The JSPWiki system user */
+    public static final Principal SYSTEM_USER = new SystemPrincipal("System User");
+
+    private final String m_name;
+
+    /**
+     * Private constructor to prevent direct instantiation.
+     * @param name the name of the Principal
+     */
+    private SystemPrincipal(String name)
+    {
+        m_name = name;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getName()
+    {
+        return m_name;
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Task.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Task.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Task.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Task.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,112 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2007 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+
+/**
+ * AbstractStep subclass that executes instructions, uninterrupted, and results
+ * in an Outcome. Concrete classes only need to implement {@link Task#execute()}.
+ * When the execution step completes, <code>execute</code> must return
+ * {@link Outcome#STEP_COMPLETE}, {@link Outcome#STEP_CONTINUE} or
+ * {@link Outcome#STEP_ABORT}. Subclasses can add any errors by calling the
+ * helper method {@link AbstractStep#addError(String)}. The execute method should
+ * <em>generally</em> capture and add errors to the error list instead of
+ * throwing a WikiException.
+ * <p>
+ *
+ * @author Andrew Jaquith
+ * @since 2.5
+ */
+public abstract class Task extends AbstractStep
+{
+    private Step m_successor = null;
+
+    /**
+     * Protected constructor that creates a new Task with a specified message key.
+     * After construction, the protected method {@link #setWorkflow(Workflow)} should be
+     * called.
+     *
+     * @param messageKey
+     *            the Step's message key, such as
+     *            <code>decision.editPageApproval</code>. By convention, the
+     *            message prefix should be a lower-case version of the Step's
+     *            type, plus a period (<em>e.g.</em>, <code>task.</code>
+     *            and <code>decision.</code>).
+     */
+    public Task( String messageKey )
+    {
+        super( messageKey );
+        super.addSuccessor(Outcome.STEP_COMPLETE, null);
+        super.addSuccessor(Outcome.STEP_ABORT, null);
+    }
+
+    /**
+     * Constructs a new instance of a Task, with an associated Workflow and
+     * message key.
+     *
+     * @param workflow
+     *            the associated workflow
+     * @param messageKey
+     *            the i18n message key
+     */
+    public Task(Workflow workflow, String messageKey)
+    {
+        this( messageKey );
+        setWorkflow( workflow );
+    }
+
+    /**
+     * Returns {@link SystemPrincipal#SYSTEM_USER}.
+     * @return the system principal
+     */
+    public final Principal getActor()
+    {
+        return SystemPrincipal.SYSTEM_USER;
+    }
+
+    /**
+     * Sets the successor Step to this one, which will be triggered if the Task
+     * completes successfully (that is, {@link Step#getOutcome()} returns
+     * {@link Outcome#STEP_COMPLETE}. This method is really a convenient
+     * shortcut for {@link Step#addSuccessor(Outcome, Step)}, where the first
+     * parameter is {@link Outcome#STEP_COMPLETE}.
+     *
+     * @param step
+     *            the successor
+     */
+    public final synchronized void setSuccessor(Step step)
+    {
+        m_successor = step;
+    }
+
+    /**
+     * Identifies the next Step after this Task finishes successfully. This
+     * method will always return the value set in method
+     * {@link #setSuccessor(Step)}, regardless of the current completion state.
+     *
+     * @return the next step
+     */
+    public final synchronized Step getSuccessor()
+    {
+        return m_successor;
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Workflow.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Workflow.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Workflow.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/workflow/Workflow.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,844 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.workflow;
+
+import java.security.Principal;
+import java.util.*;
+
+import com.ecyrd.jspwiki.WikiException;
+import com.ecyrd.jspwiki.event.WikiEventListener;
+import com.ecyrd.jspwiki.event.WikiEventManager;
+import com.ecyrd.jspwiki.event.WorkflowEvent;
+
+/**
+ * <p>
+ * Sequence of {@link Step} objects linked together. Workflows are always
+ * initialized with a message key that denotes the name of the Workflow, and a
+ * Principal that represents its owner.
+ * </p>
+ * <h2>Workflow lifecycle</h2>
+ * A Workflow's state (obtained by {@link #getCurrentState()}) will be one of the
+ * following:
+ * </p>
+ * <ul>
+ * <li><strong>{@link #CREATED}</strong>: after the Workflow has been
+ * instantiated, but before it has been started using the {@link #start()}
+ * method.</li>
+ * <li><strong>{@link #RUNNING}</strong>: after the Workflow has been started
+ * using the {@link #start()} method, but before it has finished processing all
+ * Steps. Note that a Workflow can only be started once; attempting to start it
+ * again results in an IllegalStateException. Callers can place the Workflow
+ * into the WAITING state by calling {@link #waitstate()}.</li>
+ * <li><strong>{@link #WAITING}</strong>: when the Workflow has temporarily
+ * paused, for example because of a pending Decision. Once the responsible actor
+ * decides what to do, the caller can change the Workflow back to the RUNNING
+ * state by calling the {@link #restart()} method (this is done automatically by
+ * the Decision class, for instance, when the {@link Decision#decide(Outcome)}
+ * method is invoked)</li>
+ * <li><strong>{@link #COMPLETED}</strong>: after the Workflow has finished
+ * processing all Steps, without errors.</li>
+ * <li><strong>{@link #ABORTED}</strong>: if a Step has elected to abort the
+ * Workflow.</li>
+ * </ul>
+ * <h2>Steps and processing algorithm</h2>
+ * <p>
+ * Workflow Step objects can be of type {@link Decision}, {@link Task} or other
+ * Step subclasses. Decisions require user input, while Tasks do not. See the
+ * {@link Step} class for more details.
+ * </p>
+ * <p>
+ * After instantiating a new Workflow (but before telling it to {@link #start()}),
+ * calling classes should specify the first Step by executing the
+ * {@link #setFirstStep(Step)} method. Additional Steps can be chained by
+ * invoking the first step's {@link Step#addSuccessor(Outcome, Step)} method.
+ * </p>
+ * <p>
+ * When a Workflow's <code>start</code> method is invoked, the Workflow
+ * retrieves the first Step and processes it. This Step, and subsequent ones,
+ * are processed as follows:
+ * </p>
+ * <ul>
+ * <li>The Step's {@link Step#start()} method executes, which sets the start
+ * time.</li>
+ * <li>The Step's {@link Step#execute()} method is called to begin processing,
+ * which will return an Outcome to indicate completion, continuation or errors:</li>
+ * <ul>
+ * <li>{@link Outcome#STEP_COMPLETE} indicates that the execution method ran
+ * without errors, and that the Step should be considered "completed."</li>
+ * <li>{@link Outcome#STEP_CONTINUE} indicates that the execution method ran
+ * without errors, but that the Step is not "complete" and should be put into
+ * the WAITING state.</li>
+ * <li>{@link Outcome#STEP_ABORT} indicates that the execution method
+ * encountered errors, and should abort the Step <em>and</em> the Workflow as
+ * a whole. When this happens, the Workflow will set the current Step's Outcome
+ * to {@link Outcome#STEP_ABORT} and invoke the Workflow's {@link #abort()}
+ * method. The Step's processing errors, if any, can be retrieved by
+ * {@link Step#getErrors()}.</li>
+ * </ul>
+ * <li>The Outcome of the <code>execute</code> method also affects what
+ * happens next. Depending on the result (and assuming the Step did not abort),
+ * the Workflow will either move on to the next Step or put the Workflow into
+ * the {@link Workflow#WAITING} state:</li>
+ * <ul>
+ * <li>If the Outcome denoted "completion" (<em>i.e.</em>, its
+ * {@link Step#isCompleted()} method returns <code>true</code>) then the Step
+ * is considered complete; the Workflow looks up the next Step by calling the
+ * current Step's {@link Step#getSuccessor(Outcome)} method. If
+ * <code>successor()</code> returns a non-<code>null</code> Step, the
+ * return value is marked as the current Step and added to the Workflow's Step
+ * history. If <code>successor()</code> returns <code>null</code>, then the
+ * Workflow has no more Steps and it enters the {@link #COMPLETED} state.</li>
+ * <li>If the Outcome did not denote "completion" (<em>i.e.</em>, its
+ * {@link Step#isCompleted()} method returns <code>false</code>), then the
+ * Step still has further work to do. The Workflow enters the {@link #WAITING}
+ * state and stops further processing until a caller restarts it.</li>
+ * </ul>
+ * </ul>
+ * </p>
+ * <p>
+ * The currently executing Step can be obtained by {@link #getCurrentStep()}. The
+ * actor for the current Step is returned by {@link #getCurrentActor()}.
+ * </p>
+ * <p>
+ * To provide flexibility for specific implementations, the Workflow class
+ * provides two additional features that enable Workflow participants (<em>i.e.</em>,
+ * Workflow subclasses and Step/Task/Decision subclasses) to share context and
+ * state information. These two features are <em>named attributes</em> and
+ * <em>message arguments</em>:
+ * </p>
+ * <ul>
+ * <li><strong>Named attributes</strong> are simple key-value pairs that
+ * Workflow participants can get or set. Keys are Strings; values can be any
+ * Object. Named attributes are set with {@link #setAttribute(String, Object)}
+ * and retrieved with {@link #getAttribute(String)}.</li>
+ * <li><strong>Message arguments</strong> are used in combination with
+ * JSPWiki's {@link com.ecyrd.jspwiki.i18n.InternationalizationManager} to
+ * create language-independent user interface messages. The message argument
+ * array is retrieved via {@link #getMessageArguments()}; the first two array
+ * elements will always be these: a String representing work flow owner's name,
+ * and a String representing the current actor's name. Workflow participants
+ * can add to this array by invoking {@link #addMessageArgument(Object)}.</li>
+ * </ul>
+ * <h2>Example</h2>
+ * <p>
+ * Workflow Steps can be very powerful when linked together. JSPWiki provides
+ * two abstract subclasses classes that you can use to build your own Workflows:
+ * Tasks and Decisions. As noted, Tasks are Steps that execute without user
+ * intervention, while Decisions require actors (<em>aka</em> Principals) to
+ * take action. Decisions and Tasks can be mixed freely to produce some highly
+ * elaborate branching structures.
+ * </p>
+ * <p>
+ * Here is a simple case. For example, suppose you would like to create a
+ * Workflow that (a) executes a initialization Task, (b) pauses to obtain an
+ * approval Decision from a user in the Admin group, and if approved, (c)
+ * executes a "finish" Task. Here's sample code that illustrates how to do it:
+ * </p>
+ *
+ * <pre>
+ *    // Create workflow; owner is current user
+ * 1  Workflow workflow = new Workflow(&quot;workflow.myworkflow&quot;, context.getCurrentUser());
+ *
+ *    // Create custom initialization task
+ * 2  Step initTask = new InitTask(this);
+ *
+ *    // Create finish task
+ * 3  Step finishTask = new FinishTask(this);
+ *
+ *    // Create an intermediate decision step
+ * 4  Principal actor = new GroupPrincipal(&quot;Admin&quot;);
+ * 5  Step decision = new SimpleDecision(this, &quot;decision.AdminDecision&quot;, actor);
+ *
+ *    // Hook the steps together
+ * 6  initTask.addSuccessor(Outcome.STEP_COMPLETE, decision);
+ * 7  decision.addSuccessor(Outcome.DECISION_APPROVE, finishTask);
+ *
+ *    // Set workflow's first step
+ * 8  workflow.setFirstStep(initTask);
+ * </pre>
+ *
+ * <p>
+ * Some comments on the source code:
+ * </p>
+ * <ul>
+ * <li>Line 1 instantiates the workflow with a sample message key and
+ * designated owner Principal, in this case the current wiki user</li>
+ * <li>Lines 2 and 3 instantiate the custom Task subclasses, which contain the
+ * business logic</li>
+ * <li>Line 4 creates the relevant GroupPrincipal for the <code>Admin</code>
+ * group, who will be the actor in the Decision step</li>
+ * <li>Line 5 creates the Decision step, passing the Workflow, sample message
+ * key, and actor in the constructor</li>
+ * <li>Line 6 specifies that if the InitTask's Outcome signifies "normal
+ * completion" (STEP_COMPLETE), the SimpleDecision step should be invoked next</li>
+ * <li>Line 7 specifies that if the actor (anyone possessing the
+ * <code>Admin</code> GroupPrincipal) selects DECISION_APPROVE, the FinishTask
+ * step should be invoked</li>
+ * <li>Line 8 adds the InitTask (and all of its successor Steps, nicely wired
+ * together) to the workflow</li>
+ * </ul>
+ *
+ * @author Andrew Jaquith
+ */
+public class Workflow
+{
+    /** Time value: the start or end time has not been set. */
+    public static final Date TIME_NOT_SET = new Date(0);
+
+    /** ID value: the workflow ID has not been set. */
+    public static final int ID_NOT_SET = 0;
+
+    /** State value: Workflow completed all Steps without errors. */
+    public static final int COMPLETED = 50;
+
+    /** State value: Workflow aborted before completion. */
+    public static final int ABORTED = 40;
+
+    /**
+     * State value: Workflow paused, typically because a Step returned an
+     * Outcome that doesn't signify "completion."
+     */
+    public static final int WAITING = 30;
+
+    /** State value: Workflow started, and is running. */
+    public static final int RUNNING = -1;
+
+    /** State value: Workflow instantiated, but not started. */
+    public static final int CREATED = -2;
+
+    /** Lazily-initialized attribute map. */
+    private Map<String,Object> m_attributes;
+
+    /** The initial Step for this Workflow. */
+    private Step m_firstStep;
+
+    /** Flag indicating whether the Workflow has started yet. */
+    private boolean m_started;
+
+    private final LinkedList<Step> m_history;
+
+    private int m_id;
+
+    private final String m_key;
+
+    private final Principal m_owner;
+
+    private final List<Object> m_messageArgs;
+
+    private int m_state;
+
+    private Step m_currentStep;
+
+    private WorkflowManager m_manager;
+
+    /**
+     * Constructs a new Workflow object with a supplied message key, owner
+     * Principal, and undefined unique identifier {@link #ID_NOT_SET}. Once
+     * instantiated the Workflow is considered to be in the {@link #CREATED}
+     * state; a caller must explicitly invoke the {@link #start()} method to
+     * begin processing.
+     *
+     * @param messageKey
+     *            the message key used to construct a localized workflow name,
+     *            such as <code>workflow.saveWikiPage</code>
+     * @param owner
+     *            the Principal who owns the Workflow. Typically, this is the
+     *            user who created and submitted it
+     */
+    public Workflow(String messageKey, Principal owner)
+    {
+        super();
+        m_attributes = null;
+        m_currentStep = null;
+        m_history = new LinkedList<Step>();
+        m_id = ID_NOT_SET;
+        m_key = messageKey;
+        m_manager = null;
+        m_messageArgs = new ArrayList<Object>();
+        m_owner = owner;
+        m_started = false;
+        m_state = CREATED;
+    }
+
+    /**
+     * Aborts the Workflow by setting the current Step's Outcome to
+     * {@link Outcome#STEP_ABORT}, and the Workflow's overall state to
+     * {@link #ABORTED}. It also appends the aborted Step into the workflow
+     * history, and sets the current step to <code>null</code>. If the Step
+     * is a Decision, it is removed from the DecisionQueue. This method
+     * can be called at any point in the lifecycle prior to completion, but it
+     * cannot be called twice. It finishes by calling the {@link #cleanup()}
+     * method to flush retained objects. If the Workflow had been previously
+     * aborted, this method throws an IllegalStateException.
+     */
+    public final synchronized void abort()
+    {
+        // Check corner cases: previous abort or completion
+        if (m_state == ABORTED)
+        {
+            throw new IllegalStateException("The workflow has already been aborted.");
+        }
+        if (m_state == COMPLETED)
+        {
+            throw new IllegalStateException("The workflow has already completed.");
+        }
+
+        if (m_currentStep != null)
+        {
+            if (m_manager != null && m_currentStep instanceof Decision)
+            {
+                Decision d = (Decision)m_currentStep;
+                m_manager.getDecisionQueue().remove(d);
+            }
+            m_currentStep.setOutcome(Outcome.STEP_ABORT);
+            m_history.addLast(m_currentStep);
+        }
+        m_state = ABORTED;
+        fireEvent(WorkflowEvent.ABORTED);
+        cleanup();
+    }
+
+    /**
+     * Appends a message argument object to the array returned by
+     * {@link #getMessageArguments()}. The object <em>must</em> be an type
+     * used by the {@link java.text.MessageFormat}: String, Date, or Number
+     * (BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short).
+     * If the object is not of type String, Number or Date, this method throws
+     * an IllegalArgumentException.
+     * @param obj the object to add
+     */
+    public final void addMessageArgument(Object obj)
+    {
+        if (obj instanceof String || obj instanceof Date || obj instanceof Number)
+        {
+            m_messageArgs.add(obj);
+            return;
+        }
+        throw new IllegalArgumentException("Message arguments must be of type String, Date or Number.");
+    }
+
+    /**
+     * Returns the actor Principal responsible for the current Step. If there is
+     * no current Step, this method returns <code>null</code>.
+     *
+     * @return the current actor
+     */
+    public final synchronized Principal getCurrentActor()
+    {
+        if (m_currentStep == null)
+        {
+            return null;
+        }
+        return m_currentStep.getActor();
+    }
+
+    /**
+     * Returns the workflow state: {@link #CREATED}, {@link #RUNNING},
+     * {@link #WAITING}, {@link #COMPLETED} or {@link #ABORTED}.
+     *
+     * @return the workflow state
+     */
+    public final int getCurrentState()
+    {
+        return m_state;
+    }
+
+    /**
+     * Returns the current Step, or <code>null</code> if the workflow has not
+     * started or already completed.
+     *
+     * @return the current step
+     */
+    public final Step getCurrentStep()
+    {
+        return m_currentStep;
+    }
+
+    /**
+     * Retrieves a named Object associated with this Workflow. If the Workflow
+     * has completed or aborted, this method always returns <code>null</code>.
+     *
+     * @param attr
+     *            the name of the attribute
+     * @return the value
+     */
+    public final synchronized Object getAttribute(String attr)
+    {
+        if (m_attributes == null)
+        {
+            return null;
+        }
+        return m_attributes.get(attr);
+    }
+
+    /**
+     * The end time for this Workflow, expressed as a system time number. This
+     * value is equal to the end-time value returned by the final Step's
+     * {@link Step#getEndTime()} method, if the workflow has completed.
+     * Otherwise, this method returns {@link #TIME_NOT_SET}.
+     *
+     * @return the end time
+     */
+    public final Date getEndTime()
+    {
+        if (isCompleted())
+        {
+            Step last = m_history.getLast();
+            if (last != null)
+            {
+                return last.getEndTime();
+            }
+        }
+        return TIME_NOT_SET;
+    }
+
+    /**
+     * Returns the unique identifier for this Workflow. If not set, this method
+     * returns ID_NOT_SET ({@value #ID_NOT_SET}).
+     *
+     * @return the unique identifier
+     */
+    public final synchronized int getId()
+    {
+        return m_id;
+    }
+
+    /**
+     * <p>
+     * Returns an array of message arguments, used by
+     * {@link java.text.MessageFormat} to create localized messages. The first
+     * two array elements will always be these:
+     * </p>
+     * <ul>
+     * <li>String representing the name of the workflow owner (<em>i.e.</em>,{@link #getOwner()})</li>
+     * <li>String representing the name of the current actor (<em>i.e.</em>,{@link #getCurrentActor()}).
+     * If the current step is <code>null</code> because the workflow hasn't started or has already
+     * finished, the value of this argument will be a dash character (<code>-</code>)</li>
+     * </ul>
+     * <p>
+     * Workflow and Step subclasses are free to append items to this collection
+     * with {@link #addMessageArgument(Object)}.
+     * </p>
+     *
+     * @return the array of message arguments
+     */
+    public final Object[] getMessageArguments()
+    {
+        List<Object> args = new ArrayList<Object>();
+        args.add(m_owner.getName());
+        Principal actor = getCurrentActor();
+        args.add(actor == null ? "-" : actor.getName());
+        args.addAll(m_messageArgs);
+        return args.toArray(new Object[args.size()]);
+    }
+
+    /**
+     * Returns an i18n message key for the name of this workflow; for example,
+     * <code>workflow.saveWikiPage</code>.
+     *
+     * @return the name
+     */
+    public final String getMessageKey()
+    {
+        return m_key;
+    }
+
+    /**
+     * The owner Principal on whose behalf this Workflow is being executed; that
+     * is, the user who created the workflow.
+     *
+     * @return the name of the Principal who owns this workflow
+     */
+    public final Principal getOwner()
+    {
+        return m_owner;
+    }
+
+    /**
+     * The start time for this Workflow, expressed as a system time number. This
+     * value is equal to the start-time value returned by the first Step's
+     * {@link Step#getStartTime()} method, if the workflow has started already.
+     * Otherwise, this method returns {@link #TIME_NOT_SET}.
+     *
+     * @return the start time
+     */
+    public final Date getStartTime()
+    {
+        return isStarted() ? m_firstStep.getStartTime() : TIME_NOT_SET;
+    }
+
+    /**
+     * Returns the WorkflowManager that contains this Workflow.
+     *
+     * @return the workflow manager
+     */
+    public final synchronized WorkflowManager getWorkflowManager()
+    {
+        return m_manager;
+    }
+
+    /**
+     * Returns a Step history for this Workflow as a List, chronologically, from the
+     * first Step to the currently executing one. The first step is the first
+     * item in the array. If the Workflow has not started, this method returns a
+     * zero-length array.
+     *
+     * @return an array of Steps representing those that have executed, or are
+     *         currently executing
+     */
+    public final List getHistory()
+    {
+        return Collections.unmodifiableList(m_history);
+    }
+
+    /**
+     * Returns <code>true</code> if the workflow had been previously aborted.
+     *
+     * @return the result
+     */
+    public final boolean isAborted()
+    {
+        return m_state == ABORTED;
+    }
+
+    /**
+     * Determines whether this Workflow is completed; that is, if it has no
+     * additional Steps to perform. If the last Step in the workflow is
+     * finished, this method will return <code>true</code>.
+     *
+     * @return <code>true</code> if the workflow has been started but has no
+     *         more steps to perform; <code>false</code> if not.
+     */
+    public final synchronized boolean isCompleted()
+    {
+        // If current step is null, then we're done
+        return m_started && m_state == COMPLETED;
+    }
+
+    /**
+     * Determines whether this Workflow has started; that is, its
+     * {@link #start()} method has been executed.
+     *
+     * @return <code>true</code> if the workflow has been started;
+     *         <code>false</code> if not.
+     */
+    public final boolean isStarted()
+    {
+        return m_started;
+    }
+
+    /**
+     * Convenience method that returns the predecessor of the current Step. This
+     * method simply examines the Workflow history and returns the
+     * second-to-last Step.
+     *
+     * @return the predecessor, or <code>null</code> if the first Step is
+     *         currently executing
+     */
+    public final Step getPreviousStep()
+    {
+        return previousStep(m_currentStep);
+    }
+
+    /**
+     * Restarts the Workflow from the {@link #WAITING} state and puts it into
+     * the {@link #RUNNING} state again. If the Workflow had not previously been
+     * paused, this method throws an IllegalStateException. If any of the
+     * Steps in this Workflow throw a WikiException, the Workflow will abort
+     * and propagate the exception to callers.
+     * @throws WikiException if the current task's {@link Task#execute()} method
+     * throws an exception
+     */
+    public final synchronized void restart() throws WikiException
+    {
+        if (m_state != WAITING)
+        {
+            throw new IllegalStateException("Workflow is not paused; cannot restart.");
+        }
+        m_state = RUNNING;
+        fireEvent(WorkflowEvent.RUNNING);
+
+        // Process current step
+        try
+        {
+            processCurrentStep();
+        }
+        catch ( WikiException e )
+        {
+            abort();
+            throw e;
+        }
+    }
+
+    /**
+     * Temporarily associates an Object with this Workflow, as a named attribute, for the
+     * duration of workflow execution. The passed Object can be anything required by
+     * an executing Step. Note that when the workflow completes or aborts, all
+     * attributes will be cleared.
+     *
+     * @param attr
+     *            the attribute name
+     * @param obj
+     *            the value
+     */
+    public final synchronized void setAttribute(String attr, Object obj)
+    {
+        if (m_attributes == null)
+        {
+            m_attributes = new HashMap<String,Object>();
+        }
+        m_attributes.put(attr, obj);
+    }
+
+    /**
+     * Sets the first Step for this Workflow, which will be executed immediately
+     * after the {@link #start()} method executes. Note than the Step is not
+     * marked as the "current" step or added to the Workflow history until the
+     * {@link #start()} method is called.
+     *
+     * @param step
+     *            the first step for the workflow
+     */
+    public final synchronized void setFirstStep(Step step)
+    {
+        m_firstStep = step;
+    }
+
+    /**
+     * Sets the unique identifier for this Workflow.
+     *
+     * @param id
+     *            the unique identifier
+     */
+    public final synchronized void setId(int id)
+    {
+        this.m_id = id;
+    }
+
+    /**
+     * Sets the WorkflowManager that contains this Workflow.
+     *
+     * @param manager
+     *            the workflow manager
+     */
+    public final synchronized void setWorkflowManager(WorkflowManager manager)
+    {
+        m_manager = manager;
+        addWikiEventListener(manager);
+    }
+
+    /**
+     * Starts the Workflow and sets the state to {@link #RUNNING}. If the
+     * Workflow has already been started (or previously aborted), this method
+     * returns an {@linkplain IllegalStateException}. If any of the
+     * Steps in this Workflow throw a WikiException, the Workflow will abort
+     * and propagate the exception to callers.
+     * @throws WikiException if the current Step's {@link Step#start()}
+     * method throws an exception of any kind
+     */
+    public final synchronized void start() throws WikiException
+    {
+        if (m_state == ABORTED)
+        {
+            throw new IllegalStateException("Workflow cannot be started; it has already been aborted.");
+        }
+        if (m_started)
+        {
+            throw new IllegalStateException("Workflow has already started.");
+        }
+        m_started = true;
+        m_state = RUNNING;
+        fireEvent(WorkflowEvent.RUNNING);
+
+        // Mark the first step as the current one & add to history
+        m_currentStep = m_firstStep;
+        m_history.add(m_currentStep);
+
+        // Process current step
+        try
+        {
+            processCurrentStep();
+        }
+        catch ( WikiException e )
+        {
+            abort();
+            throw e;
+        }
+    }
+
+    /**
+     * Sets the Workflow in the {@link #WAITING} state. If the Workflow is not
+     * running or has already been paused, this method throws an
+     * IllegalStateException. Once paused, the Workflow can be un-paused by
+     * executing the {@link #restart()} method.
+     */
+    public final synchronized void waitstate()
+    {
+        if (m_state != RUNNING)
+        {
+            throw new IllegalStateException("Workflow is not running; cannot pause.");
+        }
+        m_state = WAITING;
+        fireEvent(WorkflowEvent.WAITING);
+    }
+
+    /**
+     * Clears the attribute map and sets the current step field to
+     * <code>null</code>.
+     */
+    protected void cleanup()
+    {
+        m_currentStep = null;
+        m_attributes = null;
+    }
+
+    /**
+     * Protected helper method that changes the Workflow's state to
+     * {@link #COMPLETED} and sets the current Step to <code>null</code>. It
+     * calls the {@link #cleanup()} method to flush retained objects.
+     * This method will no-op if it has previously been called.
+     */
+    protected final synchronized void complete()
+    {
+        if ( !isCompleted() )
+        {
+            m_state = COMPLETED;
+            fireEvent(WorkflowEvent.COMPLETED);
+            cleanup();
+        }
+    }
+
+    /**
+     * Protected method that returns the predecessor for a supplied Step.
+     *
+     * @param step
+     *            the Step for which the predecessor is requested
+     * @return its predecessor, or <code>null</code> if the first Step was
+     *         supplied.
+     */
+    protected final Step previousStep(Step step)
+    {
+        int index = m_history.indexOf(step);
+        return index < 1 ? null : (Step) m_history.get(index - 1);
+    }
+
+    /**
+     * Protected method that processes the current Step by calling
+     * {@link Step#execute()}. If the <code>execute</code> throws an
+     * exception, this method will propagate the exception immediately
+     * to callers without aborting.
+     * @throws WikiException if the current Step's {@link Step#start()}
+     * method throws an exception of any kind
+     */
+    protected final void processCurrentStep() throws WikiException
+    {
+        while (m_currentStep != null)
+        {
+
+            // Start and execute the current step
+            if (!m_currentStep.isStarted())
+            {
+                m_currentStep.start();
+            }
+            try
+            {
+                Outcome result = m_currentStep.execute();
+                if (Outcome.STEP_ABORT.equals(result))
+                {
+                    abort();
+                    break;
+                }
+
+                if (!m_currentStep.isCompleted())
+                {
+                    m_currentStep.setOutcome(result);
+                }
+            }
+            catch (WikiException e)
+            {
+                throw e;
+            }
+
+            // Get the execution Outcome; if not complete, pause workflow and
+            // exit
+            Outcome outcome = m_currentStep.getOutcome();
+            if (!outcome.isCompletion())
+            {
+                waitstate();
+                break;
+            }
+
+            // Get the next Step; if null, we're done
+            Step nextStep = m_currentStep.getSuccessor(outcome);
+            if (nextStep == null)
+            {
+                complete();
+                break;
+            }
+
+            // Add the next step to Workflow history, and mark as current
+            m_history.add(nextStep);
+            m_currentStep = nextStep;
+        }
+
+    }
+
+    // events processing .......................................................
+
+    /**
+     * Registers a WikiEventListener with this instance. This is a convenience
+     * method.
+     *
+     * @param listener
+     *            the event listener
+     */
+    public final synchronized void addWikiEventListener(WikiEventListener listener)
+    {
+        WikiEventManager.addWikiEventListener(this, listener);
+    }
+
+    /**
+     * Un-registers a WikiEventListener with this instance. This is a
+     * convenience method.
+     *
+     * @param listener
+     *            the event listener
+     */
+    public final synchronized void removeWikiEventListener(WikiEventListener listener)
+    {
+        WikiEventManager.removeWikiEventListener(this, listener);
+    }
+
+    /**
+     * Fires a WorkflowEvent of the provided type to all registered listeners.
+     *
+     * @see com.ecyrd.jspwiki.event.WorkflowEvent
+     * @param type
+     *            the event type to be fired
+     */
+    protected final void fireEvent(int type)
+    {
+        if (WikiEventManager.isListening(this))
+        {
+            WikiEventManager.fireEvent(this, new WorkflowEvent(this, type));
+        }
+    }
+
+}