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