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