001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.workflow;
021:
022: import java.security.Principal;
023: import java.util.*;
024:
025: import com.ecyrd.jspwiki.WikiEngine;
026: import com.ecyrd.jspwiki.WikiException;
027: import com.ecyrd.jspwiki.WikiSession;
028: import com.ecyrd.jspwiki.auth.acl.UnresolvedPrincipal;
029: import com.ecyrd.jspwiki.event.WikiEvent;
030: import com.ecyrd.jspwiki.event.WikiEventListener;
031: import com.ecyrd.jspwiki.event.WorkflowEvent;
032:
033: /**
034: * <p>
035: * Monitor class that tracks running Workflows. The WorkflowManager also keeps
036: * track of the names of users or groups expected to approve particular
037: * Workflows.
038: * </p>
039: *
040: * @author Andrew Jaquith
041: */
042: public class WorkflowManager implements WikiEventListener {
043:
044: private final DecisionQueue m_queue = new DecisionQueue();
045:
046: private final Set m_workflows;
047:
048: private final Map m_approvers;
049:
050: private final List m_completed;
051:
052: /** The prefix to use for looking up <code>jspwiki.properties</code> approval roles. */
053: protected static final String PROPERTY_APPROVER_PREFIX = "jspwiki.approver.";
054:
055: /**
056: * Constructs a new WorkflowManager, with an empty workflow cache. New
057: * Workflows are automatically assigned unique identifiers, starting with 1.
058: */
059: public WorkflowManager() {
060: m_next = 1;
061: m_workflows = new HashSet();
062: m_approvers = new HashMap();
063: m_completed = new ArrayList();
064: }
065:
066: /**
067: * Adds a new workflow to the set of workflows and starts it. The new
068: * workflow is automatically assigned a unique ID. If another workflow with
069: * the same ID already exists, this method throws a WikIException.
070: * @param workflow the workflow to start
071: * @throws WikiException if a workflow the automatically assigned
072: * ID already exist; this should not happen normally
073: */
074: public void start(Workflow workflow) throws WikiException {
075: m_workflows.add(workflow);
076: workflow.setWorkflowManager(this );
077: workflow.setId(nextId());
078: workflow.start();
079: }
080:
081: /**
082: * Returns a collection of the currently active workflows.
083: *
084: * @return the current workflows
085: */
086: public Collection getWorkflows() {
087: return new HashSet(m_workflows);
088: }
089:
090: /**
091: * Returns a collection of finished workflows; that is, those that have aborted or completed.
092: * @return the finished workflows
093: */
094: public List getCompletedWorkflows() {
095: return new ArrayList(m_completed);
096: }
097:
098: private WikiEngine m_engine = null;
099:
100: /**
101: * Initializes the WorkflowManager using a specfied WikiEngine and
102: * properties. Any properties that begin with
103: * {@link #PROPERTY_APPROVER_PREFIX} will be assumed to be
104: * Decisions that require approval. For a given property key, everything
105: * after the prefix denotes the Decision's message key. The property
106: * value indicates the Principal (Role, GroupPrincipal, WikiPrincipal) that
107: * must approve the Decision. For example, if the property key/value pair
108: * is <code>jspwiki.approver.workflow.saveWikiPage=Admin</code>,
109: * the Decision's message key is <code>workflow.saveWikiPage</code>.
110: * The Principal <code>Admin</code> will be resolved via
111: * {@link com.ecyrd.jspwiki.auth.AuthorizationManager#resolvePrincipal(String)}.
112: * @param engine the wiki engine to associate with this WorkflowManager
113: * @param props the wiki engine's properties
114: */
115: public void initialize(WikiEngine engine, Properties props) {
116: m_engine = engine;
117:
118: // Identify the workflows requiring approvals
119: for (Iterator it = props.keySet().iterator(); it.hasNext();) {
120: String prop = (String) it.next();
121: if (prop.startsWith(PROPERTY_APPROVER_PREFIX)) {
122:
123: // For the key, everything after the prefix is the workflow name
124: String key = prop.substring(PROPERTY_APPROVER_PREFIX
125: .length());
126: if (key != null && key.length() > 0) {
127:
128: // Only use non-null/non-blank approvers
129: String approver = props.getProperty(prop);
130: if (approver != null && approver.length() > 0) {
131: m_approvers.put(key, new UnresolvedPrincipal(
132: approver));
133: }
134: }
135: }
136: }
137: }
138:
139: /**
140: * Returns <code>true</code> if a workflow matching a particular key
141: * contains an approval step.
142: *
143: * @param messageKey
144: * the name of the workflow; corresponds to the value returned by
145: * {@link Workflow#getMessageKey()}.
146: * @return the result
147: */
148: public boolean requiresApproval(String messageKey) {
149: return m_approvers.containsKey(messageKey);
150: }
151:
152: /**
153: * Looks up and resolves the actor who approves a Decision for a particular
154: * Workflow, based on the Workflow's message key. If not found, or if
155: * Principal is Unresolved, throws WikiException. This particular
156: * implementation always returns the GroupPrincipal <code>Admin</code>
157: *
158: * @param messageKey the Decision's message key
159: * @return the actor who approves Decisions
160: * @throws WikiException if the message key was not found, or the
161: * Principal value corresponding to the key could not be resolved
162: */
163: public Principal getApprover(String messageKey)
164: throws WikiException {
165: Principal approver = (Principal) m_approvers.get(messageKey);
166: if (approver == null) {
167: throw new WikiException("Workflow '" + messageKey
168: + "' does not require approval.");
169: }
170:
171: // Try to resolve UnresolvedPrincipals
172: if (approver instanceof UnresolvedPrincipal) {
173: String name = approver.getName();
174: approver = m_engine.getAuthorizationManager()
175: .resolvePrincipal(name);
176:
177: // If still unresolved, throw exception; otherwise, freshen our
178: // cache
179: if (approver instanceof UnresolvedPrincipal) {
180: throw new WikiException("Workflow approver '" + name
181: + "' cannot not be resolved.");
182: }
183:
184: m_approvers.put(messageKey, approver);
185: }
186: return approver;
187: }
188:
189: /**
190: * Protected helper method that returns the associated WikiEngine
191: *
192: * @return the wiki engine
193: */
194: protected WikiEngine getEngine() {
195: if (m_engine == null) {
196: throw new IllegalStateException(
197: "WikiEngine cannot be null; please initialize WorkflowManager first.");
198: }
199: return m_engine;
200: }
201:
202: /**
203: * Returns the DecisionQueue associated with this WorkflowManager
204: *
205: * @return the decision queue
206: */
207: public DecisionQueue getDecisionQueue() {
208: return m_queue;
209: }
210:
211: private volatile int m_next;
212:
213: /**
214: * Returns the next available unique identifier, which is subsequently
215: * incremented.
216: *
217: * @return the id
218: */
219: private synchronized int nextId() {
220: int current = m_next;
221: m_next++;
222: return current;
223: }
224:
225: /**
226: * Returns the current workflows a wiki session owns. These are workflows whose
227: * {@link Workflow#getOwner()} method returns a Principal also possessed by the
228: * wiki session (see {@link com.ecyrd.jspwiki.WikiSession#getPrincipals()}). If the
229: * wiki session is not authenticated, this method returns an empty Collection.
230: * @param session the wiki session
231: * @return the collection workflows the wiki session owns, which may be empty
232: */
233: public Collection getOwnerWorkflows(WikiSession session) {
234: List workflows = new ArrayList();
235: if (session.isAuthenticated()) {
236: Principal[] sessionPrincipals = session.getPrincipals();
237: for (Iterator it = m_workflows.iterator(); it.hasNext();) {
238: Workflow w = (Workflow) it.next();
239: Principal owner = w.getOwner();
240: for (int i = 0; i < sessionPrincipals.length; i++) {
241: if (sessionPrincipals[i].equals(owner)) {
242: workflows.add(w);
243: break;
244: }
245: }
246: }
247: }
248: return workflows;
249: }
250:
251: /**
252: * Listens for {@link com.ecyrd.jspwiki.event.WorkflowEvent} objects emitted
253: * by Workflows. In particular, this method listens for
254: * {@link com.ecyrd.jspwiki.event.WorkflowEvent#CREATED},
255: * {@link com.ecyrd.jspwiki.event.WorkflowEvent#ABORTED} and
256: * {@link com.ecyrd.jspwiki.event.WorkflowEvent#COMPLETED} events. If a
257: * workflow is created, it is automatically added to the cache. If one is
258: * aborted or completed, it is automatically removed.
259: * @param event the event passed to this listener
260: */
261: public void actionPerformed(WikiEvent event) {
262: if (event instanceof WorkflowEvent) {
263: Workflow workflow = (Workflow) event.getSource();
264: switch (event.getType()) {
265: case WorkflowEvent.ABORTED:
266: // Remove from manager
267: remove(workflow);
268: break;
269: case WorkflowEvent.COMPLETED:
270: // Remove from manager
271: remove(workflow);
272: break;
273: case WorkflowEvent.CREATED:
274: // Add to manager
275: add(workflow);
276: break;
277: default:
278: break;
279: }
280: }
281: }
282:
283: /**
284: * Protected helper method that adds a newly created Workflow to the cache,
285: * and sets its <code>workflowManager</code> and <code>Id</code>
286: * properties if not set.
287: *
288: * @param workflow
289: * the workflow to add
290: */
291: protected synchronized void add(Workflow workflow) {
292: if (workflow.getWorkflowManager() == null) {
293: workflow.setWorkflowManager(this );
294: }
295: if (workflow.getId() == Workflow.ID_NOT_SET) {
296: workflow.setId(nextId());
297: }
298: m_workflows.add(workflow);
299: }
300:
301: /**
302: * Protected helper method that removes a specified Workflow from the cache,
303: * and moves it to the workflow history list. This method defensively
304: * checks to see if the workflow has not yet been removed.
305: *
306: * @param workflow
307: * the workflow to remove
308: */
309: protected synchronized void remove(Workflow workflow) {
310: if (m_workflows.contains(workflow)) {
311: m_workflows.remove(workflow);
312: m_completed.add(workflow);
313: }
314: }
315: }
|