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.engine;
017
018import org.apache.log4j.MDC;
019import org.kuali.rice.coreservice.framework.parameter.ParameterService;
020import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
021import org.kuali.rice.kew.actionrequest.ActionRequestValue;
022import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
023import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
024import org.kuali.rice.kew.actions.NotificationContext;
025import org.kuali.rice.kew.actiontaken.ActionTakenValue;
026import org.kuali.rice.kew.api.WorkflowRuntimeException;
027import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
028import org.kuali.rice.kew.api.exception.WorkflowException;
029import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
030import org.kuali.rice.kew.engine.node.RouteNode;
031import org.kuali.rice.kew.engine.node.RouteNodeInstance;
032import org.kuali.rice.kew.engine.node.service.RouteNodeService;
033import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
034import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
035import org.kuali.rice.kew.service.KEWServiceLocator;
036import org.kuali.rice.kew.api.KewApiConstants;
037
038import java.util.ArrayList;
039import java.util.HashSet;
040import java.util.Iterator;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Set;
044
045
046/**
047 * A WorkflowEngine implementation which orchestrates the document through the blanket approval process.
048 *
049 * @author Kuali Rice Team (rice.collab@kuali.org)
050 */
051public class BlanketApproveEngine extends StandardWorkflowEngine {
052        
053        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BlanketApproveEngine.class);
054
055
056    BlanketApproveEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService, 
057            ParameterService parameterService, OrchestrationConfig config) {
058        super(routeNodeService, routeHeaderService, parameterService, config);
059    }
060
061    /**
062     * Orchestrates the document through the blanket approval process. The termination of the process is keyed off of the Set of node names. If there are no node names, then the document will be blanket approved past the terminal node(s) in the document.
063     */
064    public void process(String documentId, String nodeInstanceId) throws Exception {
065        if (documentId == null) {
066            throw new IllegalArgumentException("Cannot process a null document id.");
067        }
068        MDC.put("docId", documentId);
069
070        try {
071            RouteContext context = RouteContext.getCurrentRouteContext();
072            KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId);
073            if ( LOG.isInfoEnabled() ) {
074                LOG.info("Processing document for Blanket Approval: " + documentId + " : " + nodeInstanceId);
075            }
076            DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId, true);
077            if (!document.isRoutable()) {
078                //KULRICE-12283: Modified this message so it appears at a WARN level so we get better feedback if this action is skipped
079                LOG.warn("Document not routable so returning with doing no action");
080                return;
081            }
082            List<RouteNodeInstance> activeNodeInstances = new ArrayList<RouteNodeInstance>();
083            if (nodeInstanceId == null) {
084                activeNodeInstances.addAll(getRouteNodeService().getActiveNodeInstances(documentId));
085            } else {
086                RouteNodeInstance instanceNode = getRouteNodeService().findRouteNodeInstanceById(nodeInstanceId);
087                if (instanceNode == null) {
088                    throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId);
089                }
090                activeNodeInstances.add(instanceNode);
091            }
092            List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, config.getDestinationNodeNames());
093
094
095            context.setDoNotSendApproveNotificationEmails(true);
096            context.setDocument(document);
097            context.setEngineState(new EngineState());
098            NotificationContext notifyContext = null;
099            if (config.isSendNotifications()) {
100                notifyContext = new NotificationContext(KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, config.getCause().getPrincipal(), config.getCause().getActionTaken());
101            }
102            lockAdditionalDocuments(document);
103            try {
104                List<ProcessEntry> processingQueue = new LinkedList<ProcessEntry>();
105                for (RouteNodeInstance nodeInstancesToProcesses : nodeInstancesToProcess)
106                {
107                    processingQueue.add(new ProcessEntry((RouteNodeInstance) nodeInstancesToProcesses));
108                }
109                Set<String> nodesCompleted = new HashSet<String>();
110                // check the processingQueue for cases where there are no dest. nodes otherwise check if we've reached
111                // the dest. nodes
112                while (!processingQueue.isEmpty() && !isReachedDestinationNodes(config.getDestinationNodeNames(), nodesCompleted)) {
113                    ProcessEntry entry = processingQueue.remove(0);
114                    // TODO document magical join node workage (ask Eric)
115                    // TODO this has been set arbitrarily high because the implemented processing model here will probably not work for
116                    // large parallel object graphs. This needs to be re-evaluated, see KULWF-459.
117                    if (entry.getTimesProcessed() > 20) {
118                        throw new WorkflowException("Could not process document through to blanket approval." + "  Document failed to progress past node " + entry.getNodeInstance().getRouteNode().getRouteNodeName());
119                    }
120                    RouteNodeInstance nodeInstance = entry.getNodeInstance();
121                    context.setNodeInstance(nodeInstance);
122                    if (config.getDestinationNodeNames().contains(nodeInstance.getName())) {
123                        nodesCompleted.add(nodeInstance.getName());
124                        continue;
125                    }
126                    ProcessContext resultProcessContext = processNodeInstance(context, helper);
127                    invokeBlanketApproval(config.getCause(), nodeInstance, notifyContext);
128                    if (!resultProcessContext.getNextNodeInstances().isEmpty() || resultProcessContext.isComplete()) {
129                        for (Iterator nodeIt = resultProcessContext.getNextNodeInstances().iterator(); nodeIt.hasNext();) {
130                            addToProcessingQueue(processingQueue, (RouteNodeInstance) nodeIt.next());
131                        }
132                    } else {
133                        entry.increment();
134                        processingQueue.add(processingQueue.size(), entry);
135                    }
136                }
137                //clear the context so the standard engine can begin routing normally
138                RouteContext.clearCurrentRouteContext();
139                // continue with normal routing after blanket approve brings us to the correct place
140                // if there is an active approve request this is no-op.
141                super.process(documentId, null);
142            } catch (Exception e) {
143                if (e instanceof RuntimeException) {
144                        throw (RuntimeException)e;
145                } else {
146                        throw new WorkflowRuntimeException(e.toString(), e);
147                }
148            }
149        } finally {
150            RouteContext.clearCurrentRouteContext();
151            MDC.remove("docId");
152        }
153    }
154
155    /**
156     * @return true if all destination node are active but not yet complete - ready for the standard engine to take over the activation process for requests
157     */
158    private boolean isReachedDestinationNodes(Set destinationNodesNames, Set<String> nodeNamesCompleted) {
159        return !destinationNodesNames.isEmpty() && nodeNamesCompleted.equals(destinationNodesNames);
160    }
161
162    private void addToProcessingQueue(List<ProcessEntry> processingQueue, RouteNodeInstance nodeInstance) {
163        // first, detect if it's already there
164        for (ProcessEntry entry : processingQueue)
165        {
166            if (entry.getNodeInstance().getRouteNodeInstanceId().equals(nodeInstance.getRouteNodeInstanceId()))
167            {
168                entry.setNodeInstance(nodeInstance);
169                return;
170            }
171        }
172        processingQueue.add(processingQueue.size(), new ProcessEntry(nodeInstance));
173    }
174
175    /**
176     * If there are multiple paths, we need to figure out which ones we need to follow for blanket approval. This method will throw an exception if a node with the given name could not be located in the routing path. This method is written in such a way that it should be impossible for there to be an infinite loop, even if there is extensive looping in the node graph.
177     */
178    private List<RouteNodeInstance> determineNodeInstancesToProcess(List<RouteNodeInstance> activeNodeInstances, Set nodeNames) throws Exception {
179        if (nodeNames.isEmpty()) {
180            return activeNodeInstances;
181        }
182        List<RouteNodeInstance> nodeInstancesToProcess = new ArrayList<RouteNodeInstance>();
183        for (Iterator<RouteNodeInstance> iterator = activeNodeInstances.iterator(); iterator.hasNext();) {
184            RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next();
185            if (isNodeNameInPath(nodeNames, nodeInstance)) {
186                nodeInstancesToProcess.add(nodeInstance);
187            }
188        }
189        if (nodeInstancesToProcess.size() == 0) {
190            throw new InvalidActionTakenException("Could not locate nodes with the given names in the blanket approval path '" + printNodeNames(nodeNames) + "'.  " + "The document is probably already passed the specified nodes or does not contain the nodes.");
191        }
192        return nodeInstancesToProcess;
193    }
194
195    private boolean isNodeNameInPath(Set nodeNames, RouteNodeInstance nodeInstance) throws Exception {
196        boolean isInPath = false;
197        for (Object nodeName1 : nodeNames)
198        {
199            String nodeName = (String) nodeName1;
200            for (RouteNode nextNode : nodeInstance.getRouteNode().getNextNodes())
201            {
202                isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, new HashSet<String>());
203            }
204        }
205        return isInPath;
206    }
207
208    private boolean isNodeNameInPath(String nodeName, RouteNode node, Set<String> inspected) throws Exception {
209        boolean isInPath = !inspected.contains(node.getRouteNodeId()) && node.getRouteNodeName().equals(nodeName);
210        inspected.add(node.getRouteNodeId());
211        if (helper.isSubProcessNode(node)) {
212            ProcessDefinitionBo subProcess = node.getDocumentType().getNamedProcess(node.getRouteNodeName());
213            RouteNode subNode = subProcess.getInitialRouteNode();
214            if (subNode != null) {
215                isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected);
216            }
217        }
218        for (RouteNode nextNode : node.getNextNodes())
219        {
220            isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected);
221        }
222        return isInPath;
223    }
224
225    private String printNodeNames(Set nodesNames) {
226        StringBuffer buffer = new StringBuffer();
227        for (Iterator iterator = nodesNames.iterator(); iterator.hasNext();) {
228            String nodeName = (String) iterator.next();
229            buffer.append(nodeName);
230            buffer.append((iterator.hasNext() ? ", " : ""));
231        }
232        return buffer.toString();
233    }
234
235    /**
236     * Invokes the blanket approval for the given node instance. This deactivates all pending approve or complete requests at the node and sends out notifications to the individuals who's requests were trumped by the blanket approve.
237     */
238    private void invokeBlanketApproval(ActionTakenValue actionTaken, RouteNodeInstance nodeInstance, NotificationContext notifyContext) {
239        List actionRequests = getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(nodeInstance.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
240        actionRequests = getActionRequestService().getRootRequests(actionRequests);
241        List<ActionRequestValue> requestsToNotify = new ArrayList<ActionRequestValue>();
242        for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
243            ActionRequestValue request = (ActionRequestValue) iterator.next();
244            if (request.isApproveOrCompleteRequest()) {
245                requestsToNotify.add(getActionRequestService().deactivateRequest(actionTaken, request));
246            }
247            //KULRICE-12283: Added logic to deactivate acks or FYIs if a config option is provided.  This will mainly be used when a document is moved.
248            if(request.isAcknowledgeRequest() && config.isDeactivateAcknowledgements()) {
249                getActionRequestService().deactivateRequest(actionTaken, request);
250            }
251            if(request.isFYIRequest() && config.isDeactivateFYIs()) {
252                getActionRequestService().deactivateRequest(actionTaken, request);
253            }
254        }
255        if (notifyContext != null && !requestsToNotify.isEmpty()) {
256                ActionRequestFactory arFactory = new ActionRequestFactory(RouteContext.getCurrentRouteContext().getDocument(), nodeInstance);
257                KimPrincipalRecipient delegatorRecipient = null;
258                if (actionTaken.getDelegatorPrincipal() != null) {
259                        delegatorRecipient = new KimPrincipalRecipient(actionTaken.getDelegatorPrincipal());
260                }
261                List<ActionRequestValue> notificationRequests = arFactory.generateNotifications(requestsToNotify, notifyContext.getPrincipalTakingAction(), delegatorRecipient, notifyContext.getNotificationRequestCode(), notifyContext.getActionTakenCode());
262                getActionRequestService().activateRequests(notificationRequests);
263        }
264    }
265
266    private ActionRequestService getActionRequestService() {
267        return KEWServiceLocator.getActionRequestService();
268    }
269
270    private class ProcessEntry {
271
272        private RouteNodeInstance nodeInstance;
273        private int timesProcessed = 0;
274
275        public ProcessEntry(RouteNodeInstance nodeInstance) {
276            this.nodeInstance = nodeInstance;
277        }
278
279        public RouteNodeInstance getNodeInstance() {
280            return nodeInstance;
281        }
282
283        public void setNodeInstance(RouteNodeInstance nodeInstance) {
284            this.nodeInstance = nodeInstance;
285        }
286
287        public void increment() {
288            timesProcessed++;
289        }
290
291        public int getTimesProcessed() {
292            return timesProcessed;
293        }
294
295    }
296
297}