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 }