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    }