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.Iterator;
025import java.util.List;
026import java.util.Map;
027import java.util.Properties;
028import java.util.Set;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.CopyOnWriteArrayList;
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 track of the names of
044 * users or groups expected to approve particular Workflows.
045 * </p>
046 */
047public class WorkflowManager implements WikiEventListener {
048
049    /** The workflow attribute which stores the wikiContext. */
050    public static final String WF_WP_SAVE_ATTR_PRESAVE_WIKI_CONTEXT = "wikiContext";
051    /** The name of the key from jspwiki.properties which defines who shall approve the workflow of storing a wikipage.  Value is <tt>{@value}</tt> */
052    public static final String WF_WP_SAVE_APPROVER = "workflow.saveWikiPage";
053    /** The message key for storing the Decision text for saving a page.  Value is {@value}. */
054    public static final String WF_WP_SAVE_DECISION_MESSAGE_KEY = "decision.saveWikiPage";
055    /** The message key for rejecting the decision to save the page.  Value is {@value}. */
056    public static final String WF_WP_SAVE_REJECT_MESSAGE_KEY = "notification.saveWikiPage.reject";
057    /** Fact name for storing the page name.  Value is {@value}. */
058    public static final String WF_WP_SAVE_FACT_PAGE_NAME = "fact.pageName";
059    /** Fact name for storing a diff text. Value is {@value}. */
060    public static final String WF_WP_SAVE_FACT_DIFF_TEXT = "fact.diffText";
061    /** Fact name for storing the current text.  Value is {@value}. */
062    public static final String WF_WP_SAVE_FACT_CURRENT_TEXT = "fact.currentText";
063    /** Fact name for storing the proposed (edited) text.  Value is {@value}. */
064    public static final String WF_WP_SAVE_FACT_PROPOSED_TEXT = "fact.proposedText";
065    /** Fact name for storing whether the user is authenticated or not.  Value is {@value}. */
066    public static final String WF_WP_SAVE_FACT_IS_AUTHENTICATED = "fact.isAuthenticated";
067
068    /** The workflow attribute which stores the user profile. */
069    public static final String WF_UP_CREATE_SAVE_ATTR_SAVED_PROFILE = "userProfile";
070    /** The name of the key from jspwiki.properties which defines who shall approve the workflow of creating a user profile.  Value is <tt>{@value}</tt> */
071    public static final String WF_UP_CREATE_SAVE_APPROVER = "workflow.createUserProfile";
072    /** The message key for storing the Decision text for saving a user profile.  Value is {@value}. */
073    public static final String WF_UP_CREATE_SAVE_DECISION_MESSAGE_KEY = "decision.createUserProfile";
074    /** Fact name for storing a the submitter name. Value is {@value}. */
075    public static final String WF_UP_CREATE_SAVE_FACT_SUBMITTER = "fact.submitter";
076    /** Fact name for storing the preferences' login name. Value is {@value}. */
077    public static final String WF_UP_CREATE_SAVE_FACT_PREFS_LOGIN_NAME = "prefs.loginname";
078    /** Fact name for storing the preferences' full name. Value is {@value}. */
079    public static final String WF_UP_CREATE_SAVE_FACT_PREFS_FULL_NAME = "prefs.fullname";
080    /** Fact name for storing the preferences' email. Value is {@value}. */
081    public static final String WF_UP_CREATE_SAVE_FACT_PREFS_EMAIL = "prefs.email";
082
083    private final DecisionQueue m_queue = new DecisionQueue();
084
085    private final Set<Workflow> m_workflows;
086
087    private final Map<String, Principal> m_approvers;
088
089    private final List<Workflow> m_completed;
090
091    /** The prefix to use for looking up <code>jspwiki.properties</code> approval roles. */
092    protected static final String PROPERTY_APPROVER_PREFIX = "jspwiki.approver.";
093
094    /**
095     * Constructs a new WorkflowManager, with an empty workflow cache. New
096     * Workflows are automatically assigned unique identifiers, starting with 1.
097     */
098    public WorkflowManager()
099    {
100        m_next = 1;
101        m_workflows = ConcurrentHashMap.newKeySet();
102        m_approvers = new ConcurrentHashMap<>();
103        m_completed = new CopyOnWriteArrayList<>();
104    }
105
106    /**
107     * Adds a new workflow to the set of workflows and starts it. The new
108     * workflow is automatically assigned a unique ID. If another workflow with
109     * the same ID already exists, this method throws a WikIException.
110     * @param workflow the workflow to start
111     * @throws WikiException if a workflow the automatically assigned
112     * ID already exist; this should not happen normally
113     */
114    public void start( Workflow workflow ) throws WikiException
115    {
116        m_workflows.add( workflow );
117        workflow.setWorkflowManager( this );
118        workflow.setId( nextId() );
119        workflow.start();
120    }
121
122    /**
123     * Returns a collection of the currently active workflows.
124     *
125     * @return the current workflows
126     */
127    public Collection< Workflow > getWorkflows() {
128        Set< Workflow > workflows = ConcurrentHashMap.newKeySet();
129        workflows.addAll( m_workflows );
130        return workflows;
131    }
132
133    /**
134     * Returns a collection of finished workflows; that is, those that have aborted or completed.
135     * @return the finished workflows
136     */
137    public List< Workflow > getCompletedWorkflows() {
138        return new CopyOnWriteArrayList< >( m_completed );
139    }
140
141    private WikiEngine m_engine = null;
142
143    /**
144     * Initializes the WorkflowManager using a specfied WikiEngine and
145     * properties. Any properties that begin with
146     * {@link #PROPERTY_APPROVER_PREFIX} will be assumed to be
147     * Decisions that require approval. For a given property key, everything
148     * after the prefix denotes the Decision's message key. The property
149     * value indicates the Principal (Role, GroupPrincipal, WikiPrincipal) that
150     * must approve the Decision. For example, if the property key/value pair
151     * is <code>jspwiki.approver.workflow.saveWikiPage=Admin</code>,
152     * the Decision's message key is <code>workflow.saveWikiPage</code>.
153     * The Principal <code>Admin</code> will be resolved via
154     * {@link org.apache.wiki.auth.AuthorizationManager#resolvePrincipal(String)}.
155     * @param engine the wiki engine to associate with this WorkflowManager
156     * @param props the wiki engine's properties
157     */
158    public void initialize( WikiEngine engine, Properties props )
159    {
160        m_engine = engine;
161
162        // Identify the workflows requiring approvals
163        for ( Iterator<?> it = props.keySet().iterator(); it.hasNext(); )
164        {
165            String prop = (String) it.next();
166            if ( prop.startsWith( PROPERTY_APPROVER_PREFIX ) )
167            {
168
169                // For the key, everything after the prefix is the workflow name
170                String key = prop.substring( PROPERTY_APPROVER_PREFIX.length() );
171                if ( key != null && key.length() > 0 )
172                {
173
174                    // Only use non-null/non-blank approvers
175                    String approver = props.getProperty( prop );
176                    if ( approver != null && approver.length() > 0 )
177                    {
178                        m_approvers.put( key, new UnresolvedPrincipal( approver ) );
179                    }
180                }
181            }
182        }
183    }
184
185    /**
186     * Returns <code>true</code> if a workflow matching a particular key
187     * contains an approval step.
188     *
189     * @param messageKey
190     *            the name of the workflow; corresponds to the value returned by
191     *            {@link Workflow#getMessageKey()}.
192     * @return the result
193     */
194    public boolean requiresApproval( String messageKey )
195    {
196        return  m_approvers.containsKey( messageKey );
197    }
198
199    /**
200     * Looks up and resolves the actor who approves a Decision for a particular
201     * Workflow, based on the Workflow's message key. If not found, or if
202     * Principal is Unresolved, throws WikiException. This particular
203     * implementation always returns the GroupPrincipal <code>Admin</code>
204     *
205     * @param messageKey the Decision's message key
206     * @return the actor who approves Decisions
207     * @throws WikiException if the message key was not found, or the
208     * Principal value corresponding to the key could not be resolved
209     */
210    public Principal getApprover( String messageKey ) throws WikiException
211    {
212        Principal approver = m_approvers.get( messageKey );
213        if ( approver == null )
214        {
215            throw new WikiException( "Workflow '" + messageKey + "' does not require approval." );
216        }
217
218        // Try to resolve UnresolvedPrincipals
219        if ( approver instanceof UnresolvedPrincipal )
220        {
221            String name = approver.getName();
222            approver = m_engine.getAuthorizationManager().resolvePrincipal( name );
223
224            // If still unresolved, throw exception; otherwise, freshen our
225            // cache
226            if ( approver instanceof UnresolvedPrincipal )
227            {
228                throw new WikiException( "Workflow approver '" + name + "' cannot not be resolved." );
229            }
230
231            m_approvers.put( messageKey, approver );
232        }
233        return approver;
234    }
235
236    /**
237     * Protected helper method that returns the associated WikiEngine
238     *
239     * @return the wiki engine
240     */
241    protected WikiEngine getEngine()
242    {
243        if ( m_engine == null )
244        {
245            throw new IllegalStateException( "WikiEngine cannot be null; please initialize WorkflowManager first." );
246        }
247        return m_engine;
248    }
249
250    /**
251     * Returns the DecisionQueue associated with this WorkflowManager
252     *
253     * @return the decision queue
254     */
255    public DecisionQueue getDecisionQueue()
256    {
257        return m_queue;
258    }
259
260    private volatile int m_next;
261
262    /**
263     * Returns the next available unique identifier, which is subsequently
264     * incremented.
265     *
266     * @return the id
267     */
268    private synchronized int nextId()
269    {
270        int current = m_next;
271        m_next++;
272        return current;
273    }
274
275    /**
276     * Returns the current workflows a wiki session owns. These are workflows whose
277     * {@link Workflow#getOwner()} method returns a Principal also possessed by the
278     * wiki session (see {@link org.apache.wiki.WikiSession#getPrincipals()}). If the
279     * wiki session is not authenticated, this method returns an empty Collection.
280     * @param session the wiki session
281     * @return the collection workflows the wiki session owns, which may be empty
282     */
283    public Collection< Workflow > getOwnerWorkflows( WikiSession session ) {
284        List<Workflow> workflows = new ArrayList<>();
285        if ( session.isAuthenticated() ) {
286            Principal[] sessionPrincipals = session.getPrincipals();
287            for ( Workflow w : m_workflows ) {
288                Principal owner = w.getOwner();
289                for ( Principal sessionPrincipal : sessionPrincipals ) {
290                    if ( sessionPrincipal.equals( owner ) ) {
291                        workflows.add( w );
292                        break;
293                    }
294                }
295            }
296        }
297        return workflows;
298    }
299
300    /**
301     * Listens for {@link org.apache.wiki.event.WorkflowEvent} objects emitted by Workflows. In particular, this 
302     * method listens for {@link org.apache.wiki.event.WorkflowEvent#CREATED}, 
303     * {@link org.apache.wiki.event.WorkflowEvent#ABORTED} and {@link org.apache.wiki.event.WorkflowEvent#COMPLETED} 
304     * events. If a workflow is created, it is automatically added to the cache. If one is aborted or completed, it 
305     * is automatically removed.
306     * 
307     * @param event the event passed to this listener
308     */
309    @Override
310    public void actionPerformed(WikiEvent event) {
311        if (event instanceof WorkflowEvent) {
312            Workflow workflow = event.getSrc();
313            switch ( event.getType() ) {
314                case WorkflowEvent.ABORTED:
315                    // Remove from manager
316                    remove( workflow );
317                    break;
318                case WorkflowEvent.COMPLETED:
319                    // Remove from manager
320                    remove( workflow );
321                    break;
322                case WorkflowEvent.CREATED:
323                    // Add to manager
324                    add( workflow );
325                    break;
326                default:
327                    break;
328            }
329        }
330    }
331
332    /**
333     * Protected helper method that adds a newly created Workflow to the cache, and sets its 
334     * <code>workflowManager</code> and <code>Id</code> properties if not set.
335     *
336     * @param workflow the workflow to add
337     */
338    protected void add( Workflow workflow ) {
339        if ( workflow.getWorkflowManager() == null ) {
340            workflow.setWorkflowManager( this );
341        }
342        if ( workflow.getId() == Workflow.ID_NOT_SET ) {
343            workflow.setId( nextId() );
344        }
345        m_workflows.add( workflow );
346    }
347
348    /**
349     * Protected helper method that removes a specified Workflow from the cache,
350     * and moves it to the workflow history list. This method defensively
351     * checks to see if the workflow has not yet been removed.
352     *
353     * @param workflow the workflow to remove
354     */
355    protected void remove(Workflow workflow) {
356        if ( m_workflows.contains( workflow ) ) {
357            m_workflows.remove( workflow );
358            m_completed.add( workflow );
359        }
360    }
361
362}