001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kew.actions;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
021import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
022import org.kuali.rice.kew.actionrequest.ActionRequestValue;
023import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
024import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
025import org.kuali.rice.kew.actionrequest.Recipient;
026import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
027import org.kuali.rice.kew.actiontaken.ActionTakenValue;
028import org.kuali.rice.kew.api.KewApiConstants;
029import org.kuali.rice.kew.api.KewApiServiceLocator;
030import org.kuali.rice.kew.api.WorkflowRuntimeException;
031import org.kuali.rice.kew.api.action.ActionType;
032import org.kuali.rice.kew.api.doctype.DocumentTypePolicy;
033import org.kuali.rice.kew.api.document.DocumentProcessingOptions;
034import org.kuali.rice.kew.api.document.DocumentProcessingQueue;
035import org.kuali.rice.kew.api.document.attribute.DocumentAttributeIndexingQueue;
036import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
037import org.kuali.rice.kew.doctype.bo.DocumentType;
038import org.kuali.rice.kew.engine.RouteContext;
039import org.kuali.rice.kew.engine.node.RouteNodeInstance;
040import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
041import org.kuali.rice.kew.framework.postprocessor.PostProcessor;
042import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
043import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
044import org.kuali.rice.kew.service.KEWServiceLocator;
045import org.kuali.rice.kew.util.Utilities;
046import org.kuali.rice.kim.api.group.Group;
047import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
048import org.kuali.rice.kim.api.services.KimApiServiceLocator;
049import org.kuali.rice.krad.util.KRADConstants;
050
051import java.util.Collection;
052import java.util.HashSet;
053import java.util.List;
054import java.util.Set;
055import 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 */
065public 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}