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