001    /**
002     * Copyright 2005-2011 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.engine;
017    
018    import org.apache.log4j.MDC;
019    import org.kuali.rice.coreservice.framework.parameter.ParameterService;
020    import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
021    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
022    import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
023    import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
024    import org.kuali.rice.kew.actions.NotificationContext;
025    import org.kuali.rice.kew.actiontaken.ActionTakenValue;
026    import org.kuali.rice.kew.api.WorkflowRuntimeException;
027    import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
028    import org.kuali.rice.kew.api.exception.WorkflowException;
029    import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
030    import org.kuali.rice.kew.engine.node.RouteNode;
031    import org.kuali.rice.kew.engine.node.RouteNodeInstance;
032    import org.kuali.rice.kew.engine.node.service.RouteNodeService;
033    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
034    import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
035    import org.kuali.rice.kew.service.KEWServiceLocator;
036    import org.kuali.rice.kew.api.KewApiConstants;
037    
038    import java.util.ArrayList;
039    import java.util.HashSet;
040    import java.util.Iterator;
041    import java.util.LinkedList;
042    import java.util.List;
043    import 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     */
051    public 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);
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                isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected);
213            }
214            for (RouteNode nextNode : node.getNextNodes())
215            {
216                isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected);
217            }
218            return isInPath;
219        }
220    
221        private String printNodeNames(Set nodesNames) {
222            StringBuffer buffer = new StringBuffer();
223            for (Iterator iterator = nodesNames.iterator(); iterator.hasNext();) {
224                String nodeName = (String) iterator.next();
225                buffer.append(nodeName);
226                buffer.append((iterator.hasNext() ? ", " : ""));
227            }
228            return buffer.toString();
229        }
230    
231        /**
232         * 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.
233         */
234        private void invokeBlanketApproval(ActionTakenValue actionTaken, RouteNodeInstance nodeInstance, NotificationContext notifyContext) {
235            List actionRequests = getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(nodeInstance.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
236            actionRequests = getActionRequestService().getRootRequests(actionRequests);
237            List<ActionRequestValue> requestsToNotify = new ArrayList<ActionRequestValue>();
238            for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
239                ActionRequestValue request = (ActionRequestValue) iterator.next();
240                if (request.isApproveOrCompleteRequest()) {
241                    getActionRequestService().deactivateRequest(actionTaken, request);
242                    requestsToNotify.add(request);
243                }
244            }
245            if (notifyContext != null) {
246                    ActionRequestFactory arFactory = new ActionRequestFactory(RouteContext.getCurrentRouteContext().getDocument(), nodeInstance);
247                    KimPrincipalRecipient delegatorRecipient = null;
248                    if (actionTaken.getDelegatorPrincipal() != null) {
249                            delegatorRecipient = new KimPrincipalRecipient(actionTaken.getDelegatorPrincipal());
250                    }
251                    List<ActionRequestValue> notificationRequests = arFactory.generateNotifications(requestsToNotify, notifyContext.getPrincipalTakingAction(), delegatorRecipient, notifyContext.getNotificationRequestCode(), notifyContext.getActionTakenCode());
252                    getActionRequestService().activateRequests(notificationRequests);
253            }
254        }
255    
256        private ActionRequestService getActionRequestService() {
257            return KEWServiceLocator.getActionRequestService();
258        }
259    
260        private class ProcessEntry {
261    
262            private RouteNodeInstance nodeInstance;
263            private int timesProcessed = 0;
264    
265            public ProcessEntry(RouteNodeInstance nodeInstance) {
266                this.nodeInstance = nodeInstance;
267            }
268    
269            public RouteNodeInstance getNodeInstance() {
270                return nodeInstance;
271            }
272    
273            public void setNodeInstance(RouteNodeInstance nodeInstance) {
274                this.nodeInstance = nodeInstance;
275            }
276    
277            public void increment() {
278                timesProcessed++;
279            }
280    
281            public int getTimesProcessed() {
282                return timesProcessed;
283            }
284    
285        }
286    
287    }