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 }