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 */
019package org.apache.wiki.workflow;
020
021import java.security.Principal;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Properties;
030import java.util.Set;
031
032import org.apache.wiki.WikiEngine;
033import org.apache.wiki.WikiSession;
034import org.apache.wiki.api.exceptions.WikiException;
035import org.apache.wiki.auth.acl.UnresolvedPrincipal;
036import org.apache.wiki.event.WikiEvent;
037import org.apache.wiki.event.WikiEventListener;
038import 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 */
048public 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}