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("workflow.myworkflow", 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("Admin");
+ * 5 Step decision = new SimpleDecision(this, "decision.AdminDecision", 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));
+ }
+ }
+
+}