View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.actions;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
21  import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
22  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
23  import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
24  import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
25  import org.kuali.rice.kew.actionrequest.Recipient;
26  import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
27  import org.kuali.rice.kew.actiontaken.ActionTakenValue;
28  import org.kuali.rice.kew.api.KewApiConstants;
29  import org.kuali.rice.kew.api.KewApiServiceLocator;
30  import org.kuali.rice.kew.api.WorkflowRuntimeException;
31  import org.kuali.rice.kew.api.action.ActionType;
32  import org.kuali.rice.kew.api.doctype.DocumentTypePolicy;
33  import org.kuali.rice.kew.api.document.DocumentProcessingOptions;
34  import org.kuali.rice.kew.api.document.DocumentProcessingQueue;
35  import org.kuali.rice.kew.api.document.attribute.DocumentAttributeIndexingQueue;
36  import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
37  import org.kuali.rice.kew.doctype.bo.DocumentType;
38  import org.kuali.rice.kew.engine.RouteContext;
39  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
40  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
41  import org.kuali.rice.kew.framework.postprocessor.PostProcessor;
42  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
43  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
44  import org.kuali.rice.kew.service.KEWServiceLocator;
45  import org.kuali.rice.kew.util.Utilities;
46  import org.kuali.rice.kim.api.group.Group;
47  import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
48  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
49  import org.kuali.rice.krad.util.KRADConstants;
50  
51  import java.util.Collection;
52  import java.util.HashSet;
53  import java.util.List;
54  import java.util.Set;
55  import java.util.concurrent.Callable;
56  
57  /**
58   * Super class containing mostly often used methods by all actions. Holds common
59   * state as well, {@link DocumentRouteHeaderValue} document,
60   * {@link ActionTakenValue} action taken (once saved), {@link PrincipalContract} principal
61   * that has taken the action
62   *
63   * @author Kuali Rice Team (rice.collab@kuali.org)
64   */
65  public abstract class ActionTakenEvent {
66  
67      /**
68       * Default value for queueing document after the action is recorded
69       */
70      protected static final boolean DEFAULT_QUEUE_DOCUMENT_AFTER_ACTION = true;
71      /**
72       * Default value for running postprocessor logic after the action is recorded.
73       * Inspected when queueing document processing and notifying postprocessors of action taken and doc status change
74       */
75      protected static final boolean DEFAULT_RUN_POSTPROCESSOR_LOGIC = true;
76      /**
77       * Default annotation - none.
78       */
79      protected static final String DEFAULT_ANNOTATION = null;
80  
81      private static final Logger LOG = Logger.getLogger(ActionTakenEvent.class);
82  
83  
84  	/**
85  	 * Used when saving an ActionTakenValue, and for validation in validateActionRules
86       * TODO: Clarify the intent of actionTakenCode vs getActionPerformed() - which is the perceived incoming
87       * value, and what is getActionPerformed, the "value-we-are-going-to-save-to-the-db"? if so, make
88       * sure that is reflected consistently in all respective usages.
89       * See: SuperUserActionRequestApproveEvent polymorphism
90  	 */
91  	private String actionTakenCode;
92  
93  	protected final String annotation;
94  
95      /**
96       * This is in spirit immutable, however for expediency it is mutable as at least one action
97       * (ReturnToPreviousNodeAction) attempts to reload the route header after every pp notification.
98       */
99  	protected DocumentRouteHeaderValue routeHeader;
100 
101 	private final PrincipalContract principal;
102 
103     private final boolean runPostProcessorLogic;
104 
105     private final boolean queueDocumentAfterAction;
106 
107     /**
108      * This is essentially just a cache to avoid an expensive lookup in getGroupIdsForPrincipal
109      */
110     private transient List<String> groupIdsForPrincipal;
111 
112 	public ActionTakenEvent(String actionTakenCode, DocumentRouteHeaderValue routeHeader, PrincipalContract principal) {
113 		this(actionTakenCode, routeHeader, principal, DEFAULT_ANNOTATION, DEFAULT_RUN_POSTPROCESSOR_LOGIC, DEFAULT_QUEUE_DOCUMENT_AFTER_ACTION);
114 	}
115 
116     public ActionTakenEvent(String actionTakenCode, DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation) {
117         this(actionTakenCode, routeHeader, principal, annotation, DEFAULT_RUN_POSTPROCESSOR_LOGIC, DEFAULT_QUEUE_DOCUMENT_AFTER_ACTION);
118     }
119 
120 	public ActionTakenEvent(String actionTakenCode, DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean runPostProcessorLogic) {
121         this(actionTakenCode, routeHeader, principal, annotation, runPostProcessorLogic, DEFAULT_QUEUE_DOCUMENT_AFTER_ACTION);
122 	}
123 
124     public ActionTakenEvent(String actionTakenCode, DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean runPostProcessorLogic, boolean queueDocumentAfterAction) {
125         this.actionTakenCode = actionTakenCode;
126         this.routeHeader = routeHeader;
127         this.principal = principal;
128         this.annotation = annotation == null ? "" : annotation;
129         this.runPostProcessorLogic = runPostProcessorLogic;
130         this.queueDocumentAfterAction = queueDocumentAfterAction;
131     }
132 
133 	public ActionRequestService getActionRequestService() {
134 		return (ActionRequestService) KEWServiceLocator.getService(KEWServiceLocator.ACTION_REQUEST_SRV);
135 	}
136 
137 	protected DocumentRouteHeaderValue getRouteHeader() {
138 		return routeHeader;
139 	}
140 
141 	protected void setRouteHeader(DocumentRouteHeaderValue routeHeader) {
142 		this.routeHeader = routeHeader;
143 	}
144 
145     protected PrincipalContract getPrincipal() {
146 		return principal;
147 	}
148 
149 	/**
150 	 * Code of the action performed by the user
151 	 *
152 	 * Method may be overriden is action performed will be different than action
153 	 * taken
154      * @return
155      */
156 	protected String getActionPerformedCode() {
157 		return getActionTakenCode();
158 	}
159 
160 	/**
161 	 * Validates whether or not this action is valid for the given principal
162 	 * and DocumentRouteHeaderValue.
163 	 */
164 	protected boolean isActionValid() {
165 		return org.apache.commons.lang.StringUtils.isEmpty(validateActionRules());
166 	}
167 
168 
169     /**
170      * Determines whether a specific policy is set on the document type.
171      * @param docType the document type
172      * @param policy the DocumentTypePolicy
173      * @param deflt the default value if the policy is not present
174      * @return the policy value or deflt if missing
175      */
176     protected static boolean isPolicySet(DocumentType docType, DocumentTypePolicy policy, boolean deflt) {
177         return docType.getPolicyByName(policy.name(), Boolean.valueOf(deflt)).getPolicyValue().booleanValue();
178     }
179 
180     /**
181      * Determines whether a specific policy is set on the document type.
182      * @param docType the document type
183      * @param policy the DocumentTypePolicy
184      * @return the policy value or false if missing
185      */
186     protected static boolean isPolicySet(DocumentType docType, DocumentTypePolicy policy) {
187         return isPolicySet(docType, policy, false);
188     }
189 
190 	/**
191 	 * Placeholder for validation rules for each action
192 	 *
193 	 * @return error message string of specific error message
194 	 */
195 	public abstract String validateActionRules();
196 	protected abstract String validateActionRules(List<ActionRequestValue> actionRequests);
197 	
198 	/**
199 	 * Filters action requests based on if they occur after the given requestCode, and if they relate to this
200 	 * event's principal
201 	 * @param actionRequests the List of ActionRequestValues to filter
202 	 * @param requestCode the request code for all ActionRequestValues to be after
203 	 * @return the filtered List of ActionRequestValues
204 	 */
205 	protected List<ActionRequestValue> filterActionRequestsByCode(List<ActionRequestValue> actionRequests, String requestCode) {
206 		return getActionRequestService().filterActionRequestsByCode(actionRequests, getPrincipal().getPrincipalId(), getGroupIdsForPrincipal(), requestCode);
207 	}
208 
209 	protected boolean isActionCompatibleRequest(List<ActionRequestValue> requests) {
210 		LOG.debug("isActionCompatibleRequest() Default method = returning true");
211 		return true;
212 	}
213 
214     // TODO: determine why some code invokes performAction, and some code invokes record action
215     // notably, WorkflowDocumentServiceImpl. Shouldn't all invocations go through a single public entry point?
216     // are some paths implicitly trying to avoid error handling or document queueing?
217 	public void performAction() throws InvalidActionTakenException {
218 	    try{
219 	        recordAction();
220         }catch(InvalidActionTakenException e){
221             if(routeHeader.getDocumentType().getEnrouteErrorSuppression().getPolicyValue()){
222                 LOG.error("Invalid Action Taken Exception was thrown, but swallowed due to ENROUTE_ERROR_SUPPRESSION document type policy!");
223                 return;
224             }else{
225                 throw e;
226             }
227         }
228         if (queueDocumentAfterAction) {
229 	    	queueDocumentProcessing();
230 	    }
231 
232 	}
233 
234 	protected abstract void recordAction() throws InvalidActionTakenException;
235 
236 	protected void updateSearchableAttributesIfPossible() {
237 		// queue the document up so that it can be indexed for searching if it
238 		// has searchable attributes
239 		RouteContext routeContext = RouteContext.getCurrentRouteContext();
240 		if (routeHeader.getDocumentType().hasSearchableAttributes() && !routeContext.isSearchIndexingRequestedForContext()) {
241 			routeContext.requestSearchIndexingForContext();
242             DocumentAttributeIndexingQueue queue = KewApiServiceLocator.getDocumentAttributeIndexingQueue(routeHeader.getDocumentType().getApplicationId());
243             queue.indexDocument(getDocumentId());
244 		}
245 	}
246 
247     /**
248      * Wraps PostProcessor invocation with error handling
249      * @param message log message
250      * @param invocation the callable that invokes the postprocessor
251      */
252     protected void invokePostProcessor(String message, Callable<ProcessDocReport> invocation) {
253         if (!isRunPostProcessorLogic()) {
254             return;
255         }
256         LOG.debug(message);
257         try {
258             ProcessDocReport report = invocation.call();
259             if (!report.isSuccess()) {
260                 LOG.warn(report.getMessage(), report.getProcessException());
261                 throw new InvalidActionTakenException(report.getMessage());
262             }
263         } catch (Exception ex) {
264             processPostProcessorException(ex);
265         }
266     }
267 
268 	protected void notifyActionTaken(final ActionTakenValue actionTaken) {
269         invokePostProcessor("Notifying post processor of action taken", new Callable<ProcessDocReport>() {
270             public ProcessDocReport call() throws Exception {
271                 PostProcessor postProcessor = routeHeader.getDocumentType().getPostProcessor();
272                 return postProcessor.doActionTaken(new org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent(routeHeader.getDocumentId(), routeHeader.getAppDocId(), ActionTakenValue.to(actionTaken)));
273             }
274         });
275 	}
276 
277     protected void notifyAfterActionTaken(final ActionTakenValue actionTaken) {
278         invokePostProcessor("Notifying post processor after action taken", new Callable<ProcessDocReport>() {
279             public ProcessDocReport call() throws Exception {
280                 PostProcessor postProcessor = routeHeader.getDocumentType().getPostProcessor();
281                 return postProcessor.afterActionTaken(ActionType.fromCode(getActionPerformedCode()), new org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent(routeHeader.getDocumentId(), routeHeader.getAppDocId(), ActionTakenValue.to(actionTaken)));
282             }
283         });
284     }
285 
286 	protected void notifyStatusChange(final String newStatusCode, final String oldStatusCode) throws InvalidActionTakenException {
287         invokePostProcessor("Notifying post processor of status change " + oldStatusCode + "->" + newStatusCode, new Callable<ProcessDocReport>() {
288             public ProcessDocReport call() throws Exception {
289                 DocumentRouteStatusChange statusChangeEvent = new DocumentRouteStatusChange(routeHeader.getDocumentId(), routeHeader.getAppDocId(), oldStatusCode, newStatusCode);
290                 PostProcessor postProcessor = routeHeader.getDocumentType().getPostProcessor();
291                 return postProcessor.doRouteStatusChange(statusChangeEvent);
292             }
293         });
294 	}
295 
296 	/**
297 	 * Asynchronously queues the documented to be processed by the workflow engine.
298 	 */
299 	protected void queueDocumentProcessing() {
300     	DocumentRouteHeaderValue document = getRouteHeader();
301         String applicationId = document.getDocumentType().getApplicationId();
302         DocumentProcessingQueue documentProcessingQueue = (DocumentProcessingQueue) KewApiServiceLocator.getDocumentProcessingQueue(
303             document.getDocumentId(), applicationId);
304         DocumentProcessingOptions options = DocumentProcessingOptions.create(isRunPostProcessorLogic(), RouteContext.getCurrentRouteContext().isSearchIndexingRequestedForContext());
305         documentProcessingQueue.processWithOptions(getDocumentId(), options);
306 	}
307 
308 	protected ActionTakenValue saveActionTaken() {
309 	    return saveActionTaken(Boolean.TRUE);
310 	}
311 
312 	protected ActionTakenValue saveActionTaken(Boolean currentInd) {
313 		return saveActionTaken(currentInd, null);
314 	}
315 
316 	protected ActionTakenValue saveActionTaken(Recipient delegator) {
317 	    return saveActionTaken(Boolean.TRUE, delegator);
318 	}
319 
320 	protected ActionTakenValue saveActionTaken(Boolean currentInd, Recipient delegator) {
321 		ActionTakenValue val = new ActionTakenValue();
322 		val.setActionTaken(getActionTakenCode());
323 		val.setAnnotation(annotation);
324 		val.setDocVersion(routeHeader.getDocVersion());
325 		val.setDocumentId(routeHeader.getDocumentId());
326 		val.setPrincipalId(principal.getPrincipalId());
327 		if (delegator instanceof KimPrincipalRecipient) {
328 			val.setDelegatorPrincipalId(((KimPrincipalRecipient)delegator).getPrincipalId());
329 		} else if (delegator instanceof KimGroupRecipient) {
330 			val.setDelegatorGroupId(((KimGroupRecipient) delegator).getGroupId());
331 		}
332 		//val.setRouteHeader(routeHeader);
333 		val.setCurrentIndicator(currentInd);
334 		val = KEWServiceLocator.getActionTakenService().saveActionTaken(val);
335 		return val;
336 	}
337 
338 	/**
339 	 * Returns the highest priority delegator in the list of action requests.
340 	 */
341 	protected Recipient findDelegatorForActionRequests(List actionRequests) {
342 		return getActionRequestService().findDelegator(actionRequests);
343 	}
344 
345 	public String getActionTakenCode() {
346 		return actionTakenCode;
347 	}
348 
349 	protected void setActionTakenCode(String string) {
350 		actionTakenCode = string;
351 	}
352 
353 	protected String getDocumentId() {
354 		return this.routeHeader.getDocumentId();
355 	}
356 
357 	/*protected void delete() {
358 	    KEWServiceLocator.getActionTakenService().delete(actionTaken);
359 	}*/
360 
361 	protected boolean isRunPostProcessorLogic() {
362         return this.runPostProcessorLogic;
363     }
364 	
365 	protected List<String> getGroupIdsForPrincipal() {
366 		if (groupIdsForPrincipal == null) {
367 			groupIdsForPrincipal = KimApiServiceLocator.getGroupService().getGroupIdsByPrincipalId(
368                     getPrincipal().getPrincipalId());
369 		}
370 		return groupIdsForPrincipal;
371 	}
372 
373 	private void processPostProcessorException(Exception e) {
374         if (e instanceof RuntimeException) {
375             throw (RuntimeException)e;
376         }
377         throw new WorkflowRuntimeException(e);
378 	}
379 
380     /**
381      * Utility for generating Acknowledgements to previous document action takers.  Note that in constrast with other
382      * notification-generation methods (such as those in ActionRequestFactory) this method determines its recipient list
383      * from ActionTakenValues, not from outstanding ActionRequests.
384      * @see ActionRequestFactory#generateNotifications(java.util.List, org.kuali.rice.kim.api.identity.principal.PrincipalContract, org.kuali.rice.kew.actionrequest.Recipient, String, String)
385      * @see ActionRequestFactory#generateNotifications(org.kuali.rice.kew.actionrequest.ActionRequestValue, java.util.List, org.kuali.rice.kim.api.identity.principal.PrincipalContract, org.kuali.rice.kew.actionrequest.Recipient, String, String, org.kuali.rice.kim.api.group.Group)
386      * @param notificationNodeInstance the node instance with which generated actionrequests will be associated
387      */
388     protected void generateAcknowledgementsToPreviousActionTakers(RouteNodeInstance notificationNodeInstance)
389     {
390         String groupName = CoreFrameworkServiceLocator.getParameterService().getParameterValueAsString(
391                 KewApiConstants.KEW_NAMESPACE,
392                 KRADConstants.DetailTypes.WORKGROUP_DETAIL_TYPE,
393                 KewApiConstants.NOTIFICATION_EXCLUDED_USERS_WORKGROUP_NAME_IND);
394 
395         Set<String> systemPrincipalIds = new HashSet<String>();
396 
397         if( !StringUtils.isBlank(groupName))
398         {
399             Group systemUserWorkgroup = KimApiServiceLocator.getGroupService().
400                     getGroupByNamespaceCodeAndName(Utilities.parseGroupNamespaceCode(groupName),
401                             Utilities.parseGroupName(groupName));
402 
403             List<String> principalIds = KimApiServiceLocator.
404                     getGroupService().getMemberPrincipalIds( systemUserWorkgroup.getId());
405 
406             if (systemUserWorkgroup != null)
407             {
408                 for( String id : principalIds)
409                 {
410                     systemPrincipalIds.add(id);
411                 }
412             }
413         }
414         ActionRequestFactory arFactory = new ActionRequestFactory(getRouteHeader(), notificationNodeInstance);
415         Collection<ActionTakenValue> actions = KEWServiceLocator.getActionTakenService().findByDocumentId(getDocumentId());
416 
417         //one notification per person, also, it would be silly for us to notify the person who just took the action, so don't include them in the notification
418         Set<String> usersNotified = new HashSet<String>();
419         usersNotified.add(getPrincipal().getPrincipalId());
420 
421         for (ActionTakenValue action : actions)
422         {
423             if ((action.isApproval() || action.isCompletion()) && !usersNotified.contains(action.getPrincipalId()))
424             {
425                 if (!systemPrincipalIds.contains(action.getPrincipalId()))
426                 {
427                     ActionRequestValue request = arFactory.createNotificationRequest(KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, action.getPrincipal(), getActionTakenCode(), getPrincipal(), getActionTakenCode());
428                     request = KEWServiceLocator.getActionRequestService().activateRequest(request);
429                     usersNotified.add(request.getPrincipalId());
430                 }
431             }
432         }
433     }
434 }