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}