001 /** 002 * Copyright 2005-2014 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.rice.kew.actions; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.apache.log4j.Logger; 020 import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator; 021 import org.kuali.rice.kew.actionrequest.ActionRequestFactory; 022 import org.kuali.rice.kew.actionrequest.ActionRequestValue; 023 import org.kuali.rice.kew.actionrequest.KimGroupRecipient; 024 import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient; 025 import org.kuali.rice.kew.actionrequest.Recipient; 026 import org.kuali.rice.kew.actionrequest.service.ActionRequestService; 027 import org.kuali.rice.kew.actiontaken.ActionTakenValue; 028 import org.kuali.rice.kew.api.KewApiConstants; 029 import org.kuali.rice.kew.api.KewApiServiceLocator; 030 import org.kuali.rice.kew.api.WorkflowRuntimeException; 031 import org.kuali.rice.kew.api.action.ActionType; 032 import org.kuali.rice.kew.api.doctype.DocumentTypePolicy; 033 import org.kuali.rice.kew.api.document.DocumentProcessingOptions; 034 import org.kuali.rice.kew.api.document.DocumentProcessingQueue; 035 import org.kuali.rice.kew.api.document.attribute.DocumentAttributeIndexingQueue; 036 import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 037 import org.kuali.rice.kew.doctype.bo.DocumentType; 038 import org.kuali.rice.kew.engine.RouteContext; 039 import org.kuali.rice.kew.engine.node.RouteNodeInstance; 040 import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 041 import org.kuali.rice.kew.framework.postprocessor.PostProcessor; 042 import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 043 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 044 import org.kuali.rice.kew.service.KEWServiceLocator; 045 import org.kuali.rice.kew.util.Utilities; 046 import org.kuali.rice.kim.api.group.Group; 047 import org.kuali.rice.kim.api.identity.principal.PrincipalContract; 048 import org.kuali.rice.kim.api.services.KimApiServiceLocator; 049 import org.kuali.rice.krad.util.KRADConstants; 050 051 import java.util.Collection; 052 import java.util.HashSet; 053 import java.util.List; 054 import java.util.Set; 055 import java.util.concurrent.Callable; 056 057 /** 058 * Super class containing mostly often used methods by all actions. Holds common 059 * state as well, {@link DocumentRouteHeaderValue} document, 060 * {@link ActionTakenValue} action taken (once saved), {@link PrincipalContract} principal 061 * that has taken the action 062 * 063 * @author Kuali Rice Team (rice.collab@kuali.org) 064 */ 065 public abstract class ActionTakenEvent { 066 067 /** 068 * Default value for queueing document after the action is recorded 069 */ 070 protected static final boolean DEFAULT_QUEUE_DOCUMENT_AFTER_ACTION = true; 071 /** 072 * Default value for running postprocessor logic after the action is recorded. 073 * Inspected when queueing document processing and notifying postprocessors of action taken and doc status change 074 */ 075 protected static final boolean DEFAULT_RUN_POSTPROCESSOR_LOGIC = true; 076 /** 077 * Default annotation - none. 078 */ 079 protected static final String DEFAULT_ANNOTATION = null; 080 081 private static final Logger LOG = Logger.getLogger(ActionTakenEvent.class); 082 083 084 /** 085 * Used when saving an ActionTakenValue, and for validation in validateActionRules 086 * TODO: Clarify the intent of actionTakenCode vs getActionPerformed() - which is the perceived incoming 087 * value, and what is getActionPerformed, the "value-we-are-going-to-save-to-the-db"? if so, make 088 * sure that is reflected consistently in all respective usages. 089 * See: SuperUserActionRequestApproveEvent polymorphism 090 */ 091 private String actionTakenCode; 092 093 protected final String annotation; 094 095 /** 096 * This is in spirit immutable, however for expediency it is mutable as at least one action 097 * (ReturnToPreviousNodeAction) attempts to reload the route header after every pp notification. 098 */ 099 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 }