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    }