Coverage Report - org.kuali.rice.kew.engine.BlanketApproveEngine
 
Classes in this File Line Coverage Branch Coverage Complexity
BlanketApproveEngine
0%
0/139
0%
0/84
3.35
BlanketApproveEngine$ProcessEntry
0%
0/10
N/A
3.35
 
 1  
 /*
 2  
  * Copyright 2005-2007 The Kuali Foundation
 3  
  *
 4  
  *
 5  
  * Licensed under the Educational Community License, Version 2.0 (the "License");
 6  
  * you may not use this file except in compliance with the License.
 7  
  * You may obtain a copy of the License at
 8  
  *
 9  
  * http://www.opensource.org/licenses/ecl2.php
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 package org.kuali.rice.kew.engine;
 18  
 
 19  
 import org.apache.log4j.MDC;
 20  
 import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
 21  
 import org.kuali.rice.kew.actionrequest.ActionRequestValue;
 22  
 import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
 23  
 import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
 24  
 import org.kuali.rice.kew.actions.NotificationContext;
 25  
 import org.kuali.rice.kew.actiontaken.ActionTakenValue;
 26  
 import org.kuali.rice.kew.engine.node.Process;
 27  
 import org.kuali.rice.kew.engine.node.RouteNode;
 28  
 import org.kuali.rice.kew.engine.node.RouteNodeInstance;
 29  
 import org.kuali.rice.kew.exception.InvalidActionTakenException;
 30  
 import org.kuali.rice.kew.exception.WorkflowException;
 31  
 import org.kuali.rice.kew.exception.WorkflowRuntimeException;
 32  
 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
 33  
 import org.kuali.rice.kew.service.KEWServiceLocator;
 34  
 import org.kuali.rice.kew.util.KEWConstants;
 35  
 import org.kuali.rice.kew.util.Utilities;
 36  
 
 37  
 import java.util.*;
 38  
 
 39  
 
 40  
 /**
 41  
  * A WorkflowEngine implementation which orchestrates the document through the blanket approval process.
 42  
  *
 43  
  * @author Kuali Rice Team (rice.collab@kuali.org)
 44  
  */
 45  
 public class BlanketApproveEngine extends StandardWorkflowEngine {
 46  0
         private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BlanketApproveEngine.class);
 47  
 
 48  
     // private Set nodeNames;
 49  
     // private ActionTakenValue actionTaken;
 50  
 //    private ActionRequestNotificationGenerator notifier = new ActionRequestNotificationGenerator();
 51  
     private OrchestrationConfig config;
 52  
 
 53  
     public BlanketApproveEngine(OrchestrationConfig config, boolean runPostProcessorLogic) {
 54  0
         super(runPostProcessorLogic);
 55  0
         this.config = config;
 56  0
     }
 57  
 
 58  0
     public BlanketApproveEngine(OrchestrationConfig config) {
 59  0
         this.config = config;
 60  0
     }
 61  
 
 62  
     public BlanketApproveEngine(String nodeName, ActionTakenValue actionTaken) {
 63  0
         this(wrapInSet(nodeName), actionTaken);
 64  0
     }
 65  
 
 66  
     public BlanketApproveEngine(Set nodeNames, ActionTakenValue actionTaken) {
 67  0
         this(createBlanketApproveConfig(nodeNames, actionTaken));
 68  0
     }
 69  
 
 70  
     private static Set<String> wrapInSet(String nodeName) {
 71  0
         Set<String> nodeNames = new HashSet<String>();
 72  0
         if (!Utilities.isEmpty(nodeName)) {
 73  0
             nodeNames.add(nodeName);
 74  
         }
 75  0
         return nodeNames;
 76  
     }
 77  
 
 78  
     private static OrchestrationConfig createBlanketApproveConfig(Set nodeNames, ActionTakenValue actionTaken) {
 79  0
         OrchestrationConfig config = new OrchestrationConfig();
 80  0
         config.setCause(actionTaken);
 81  0
         config.setDestinationNodeNames(nodeNames);
 82  0
         config.setNotificationType(KEWConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ);
 83  0
         config.setSendNotifications(true);
 84  0
         return config;
 85  
     }
 86  
 
 87  
     /**
 88  
      * 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.
 89  
      */
 90  
     public void process(Long documentId, Long nodeInstanceId) throws Exception {
 91  0
         if (documentId == null) {
 92  0
             throw new IllegalArgumentException("Cannot process a null document id.");
 93  
         }
 94  0
         MDC.put("docId", documentId);
 95  0
         RouteContext context = RouteContext.getCurrentRouteContext();
 96  
         try {
 97  0
             KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId, true);
 98  0
             if ( LOG.isInfoEnabled() ) {
 99  0
                     LOG.info("Processing document for Blanket Approval: " + documentId + " : " + nodeInstanceId);
 100  
             }
 101  0
             DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId);
 102  0
             if (!document.isRoutable()) {
 103  0
                 LOG.debug("Document not routable so returning with doing no action");
 104  0
                 return;
 105  
             }
 106  0
             List<RouteNodeInstance> activeNodeInstances = new ArrayList<RouteNodeInstance>();
 107  0
             if (nodeInstanceId == null) {
 108  0
                 activeNodeInstances.addAll(getRouteNodeService().getActiveNodeInstances(documentId));
 109  
             } else {
 110  0
                 RouteNodeInstance instanceNode = getRouteNodeService().findRouteNodeInstanceById(nodeInstanceId);
 111  0
                 if (instanceNode == null) {
 112  0
                     throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId);
 113  
                 }
 114  0
                 activeNodeInstances.add(instanceNode);
 115  
             }
 116  0
             List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, config.getDestinationNodeNames());
 117  
 
 118  
 
 119  0
             context.setDoNotSendApproveNotificationEmails(true);
 120  0
             context.setDocument(document);
 121  0
             context.setEngineState(new EngineState());
 122  0
             NotificationContext notifyContext = null;
 123  0
             if (config.isSendNotifications()) {
 124  0
                 notifyContext = new NotificationContext(KEWConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, config.getCause().getPrincipal(), config.getCause().getActionTaken());
 125  
             }
 126  0
             lockAdditionalDocuments(document);
 127  
             try {
 128  0
                 List<ProcessEntry> processingQueue = new LinkedList<ProcessEntry>();
 129  0
                 for (RouteNodeInstance nodeInstancesToProcesses : nodeInstancesToProcess)
 130  
                 {
 131  0
                     processingQueue.add(new ProcessEntry((RouteNodeInstance) nodeInstancesToProcesses));
 132  
                 }
 133  0
                 Set<String> nodesCompleted = new HashSet<String>();
 134  
                 // check the processingQueue for cases where there are no dest. nodes otherwise check if we've reached
 135  
                 // the dest. nodes
 136  0
                 while (!processingQueue.isEmpty() && !isReachedDestinationNodes(config.getDestinationNodeNames(), nodesCompleted)) {
 137  0
                     ProcessEntry entry = processingQueue.remove(0);
 138  
                     // TODO document magical join node workage (ask Eric)
 139  
                     // TODO this has been set arbitrarily high because the implemented processing model here will probably not work for
 140  
                     // large parallel object graphs. This needs to be re-evaluated, see KULWF-459.
 141  0
                     if (entry.getTimesProcessed() > 20) {
 142  0
                         throw new WorkflowException("Could not process document through to blanket approval." + "  Document failed to progress past node " + entry.getNodeInstance().getRouteNode().getRouteNodeName());
 143  
                     }
 144  0
                     RouteNodeInstance nodeInstance = entry.getNodeInstance();
 145  0
                     context.setNodeInstance(nodeInstance);
 146  0
                     if (config.getDestinationNodeNames().contains(nodeInstance.getName())) {
 147  0
                         nodesCompleted.add(nodeInstance.getName());
 148  0
                         continue;
 149  
                     }
 150  0
                     ProcessContext resultProcessContext = processNodeInstance(context, helper);
 151  0
                     invokeBlanketApproval(config.getCause(), nodeInstance, notifyContext);
 152  0
                     if (!resultProcessContext.getNextNodeInstances().isEmpty() || resultProcessContext.isComplete()) {
 153  0
                         for (Iterator nodeIt = resultProcessContext.getNextNodeInstances().iterator(); nodeIt.hasNext();) {
 154  0
                             addToProcessingQueue(processingQueue, (RouteNodeInstance) nodeIt.next());
 155  
                         }
 156  
                     } else {
 157  0
                         entry.increment();
 158  0
                         processingQueue.add(processingQueue.size(), entry);
 159  
                     }
 160  0
                 }
 161  
                 //clear the context so the standard engine can begin routing normally
 162  0
                 RouteContext.clearCurrentRouteContext();
 163  
                 // continue with normal routing after blanket approve brings us to the correct place
 164  
                 // if there is an active approve request this is no-op.
 165  0
                 super.process(documentId, null);
 166  0
             } catch (Exception e) {
 167  0
                     if (e instanceof RuntimeException) {
 168  0
                         throw (RuntimeException)e;
 169  
                 } else {
 170  0
                         throw new WorkflowRuntimeException(e.toString(), e);
 171  
                 }
 172  0
             }
 173  0
         } finally {
 174  0
                 RouteContext.clearCurrentRouteContext();
 175  0
             MDC.remove("docId");
 176  0
         }
 177  0
     }
 178  
 
 179  
     /**
 180  
      * @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
 181  
      */
 182  
     private boolean isReachedDestinationNodes(Set destinationNodesNames, Set<String> nodeNamesCompleted) {
 183  0
         return !destinationNodesNames.isEmpty() && nodeNamesCompleted.equals(destinationNodesNames);
 184  
     }
 185  
 
 186  
     private void addToProcessingQueue(List<ProcessEntry> processingQueue, RouteNodeInstance nodeInstance) {
 187  
         // first, detect if it's already there
 188  0
         for (ProcessEntry entry : processingQueue)
 189  
         {
 190  0
             if (entry.getNodeInstance().getRouteNodeInstanceId().equals(nodeInstance.getRouteNodeInstanceId()))
 191  
             {
 192  0
                 entry.setNodeInstance(nodeInstance);
 193  0
                 return;
 194  
             }
 195  
         }
 196  0
         processingQueue.add(processingQueue.size(), new ProcessEntry(nodeInstance));
 197  0
     }
 198  
 
 199  
     /**
 200  
      * 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.
 201  
      */
 202  
     private List<RouteNodeInstance> determineNodeInstancesToProcess(List<RouteNodeInstance> activeNodeInstances, Set nodeNames) throws Exception {
 203  0
         if (nodeNames.isEmpty()) {
 204  0
             return activeNodeInstances;
 205  
         }
 206  0
         List<RouteNodeInstance> nodeInstancesToProcess = new ArrayList<RouteNodeInstance>();
 207  0
         for (Iterator<RouteNodeInstance> iterator = activeNodeInstances.iterator(); iterator.hasNext();) {
 208  0
             RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next();
 209  0
             if (isNodeNameInPath(nodeNames, nodeInstance)) {
 210  0
                 nodeInstancesToProcess.add(nodeInstance);
 211  
             }
 212  0
         }
 213  0
         if (nodeInstancesToProcess.size() == 0) {
 214  0
             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.");
 215  
         }
 216  0
         return nodeInstancesToProcess;
 217  
     }
 218  
 
 219  
     private boolean isNodeNameInPath(Set nodeNames, RouteNodeInstance nodeInstance) throws Exception {
 220  0
         boolean isInPath = false;
 221  0
         for (Object nodeName1 : nodeNames)
 222  
         {
 223  0
             String nodeName = (String) nodeName1;
 224  0
             for (RouteNode nextNode : nodeInstance.getRouteNode().getNextNodes())
 225  
             {
 226  0
                 isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, new HashSet<Long>());
 227  
             }
 228  0
         }
 229  0
         return isInPath;
 230  
     }
 231  
 
 232  
     private boolean isNodeNameInPath(String nodeName, RouteNode node, Set<Long> inspected) throws Exception {
 233  0
         boolean isInPath = !inspected.contains(node.getRouteNodeId()) && node.getRouteNodeName().equals(nodeName);
 234  0
         inspected.add(node.getRouteNodeId());
 235  0
         if (helper.isSubProcessNode(node)) {
 236  0
             Process subProcess = node.getDocumentType().getNamedProcess(node.getRouteNodeName());
 237  0
             RouteNode subNode = subProcess.getInitialRouteNode();
 238  0
             isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected);
 239  
         }
 240  0
         for (RouteNode nextNode : node.getNextNodes())
 241  
         {
 242  0
             isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected);
 243  
         }
 244  0
         return isInPath;
 245  
     }
 246  
 
 247  
     private String printNodeNames(Set nodesNames) {
 248  0
         StringBuffer buffer = new StringBuffer();
 249  0
         for (Iterator iterator = nodesNames.iterator(); iterator.hasNext();) {
 250  0
             String nodeName = (String) iterator.next();
 251  0
             buffer.append(nodeName);
 252  0
             buffer.append((iterator.hasNext() ? ", " : ""));
 253  0
         }
 254  0
         return buffer.toString();
 255  
     }
 256  
 
 257  
     /**
 258  
      * 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.
 259  
      */
 260  
     private void invokeBlanketApproval(ActionTakenValue actionTaken, RouteNodeInstance nodeInstance, NotificationContext notifyContext) {
 261  0
         List actionRequests = getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(nodeInstance.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
 262  0
         actionRequests = getActionRequestService().getRootRequests(actionRequests);
 263  0
         List<ActionRequestValue> requestsToNotify = new ArrayList<ActionRequestValue>();
 264  0
         for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
 265  0
             ActionRequestValue request = (ActionRequestValue) iterator.next();
 266  0
             if (request.isApproveOrCompleteRequest()) {
 267  0
                 getActionRequestService().deactivateRequest(actionTaken, request);
 268  0
                 requestsToNotify.add(request);
 269  
             }
 270  0
         }
 271  0
         if (notifyContext != null) {
 272  0
                 ActionRequestFactory arFactory = new ActionRequestFactory(RouteContext.getCurrentRouteContext().getDocument(), nodeInstance);
 273  0
                 KimPrincipalRecipient delegatorRecipient = null;
 274  0
                 if (actionTaken.getDelegatorPrincipal() != null) {
 275  0
                         delegatorRecipient = new KimPrincipalRecipient(actionTaken.getDelegatorPrincipal());
 276  
                 }
 277  0
                 List<ActionRequestValue> notificationRequests = arFactory.generateNotifications(requestsToNotify, notifyContext.getPrincipalTakingAction(), delegatorRecipient, notifyContext.getNotificationRequestCode(), notifyContext.getActionTakenCode());
 278  0
                 getActionRequestService().activateRequests(notificationRequests);
 279  
         }
 280  0
     }
 281  
 
 282  
     private ActionRequestService getActionRequestService() {
 283  0
         return KEWServiceLocator.getActionRequestService();
 284  
     }
 285  
 
 286  
     private class ProcessEntry {
 287  
 
 288  
         private RouteNodeInstance nodeInstance;
 289  0
         private int timesProcessed = 0;
 290  
 
 291  0
         public ProcessEntry(RouteNodeInstance nodeInstance) {
 292  0
             this.nodeInstance = nodeInstance;
 293  0
         }
 294  
 
 295  
         public RouteNodeInstance getNodeInstance() {
 296  0
             return nodeInstance;
 297  
         }
 298  
 
 299  
         public void setNodeInstance(RouteNodeInstance nodeInstance) {
 300  0
             this.nodeInstance = nodeInstance;
 301  0
         }
 302  
 
 303  
         public void increment() {
 304  0
             timesProcessed++;
 305  0
         }
 306  
 
 307  
         public int getTimesProcessed() {
 308  0
             return timesProcessed;
 309  
         }
 310  
 
 311  
     }
 312  
 
 313  
 }