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.node; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.apache.log4j.MDC; 020 import org.kuali.rice.core.api.criteria.Predicate; 021 import org.kuali.rice.core.api.criteria.QueryByCriteria; 022 import org.kuali.rice.kew.actionitem.ActionItem; 023 import org.kuali.rice.kew.actionrequest.ActionRequestValue; 024 import org.kuali.rice.kew.api.action.ActionRequestStatus; 025 import org.kuali.rice.kew.doctype.bo.DocumentType; 026 import org.kuali.rice.kew.engine.RouteContext; 027 import org.kuali.rice.kew.engine.RouteHelper; 028 import org.kuali.rice.kew.exception.RouteManagerException; 029 import org.kuali.rice.kew.api.exception.WorkflowException; 030 import org.kuali.rice.kew.role.RoleRouteModule; 031 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 032 import org.kuali.rice.kew.routemodule.RouteModule; 033 import org.kuali.rice.kew.service.KEWServiceLocator; 034 import org.kuali.rice.kew.util.ClassDumper; 035 import org.kuali.rice.kew.api.KewApiConstants; 036 import org.kuali.rice.kew.util.PerformanceLogger; 037 import org.kuali.rice.kim.api.KimConstants; 038 import org.kuali.rice.kim.api.responsibility.Responsibility; 039 import org.kuali.rice.kim.api.services.KimApiServiceLocator; 040 import org.kuali.rice.krad.util.KRADConstants; 041 042 import java.util.ArrayList; 043 import java.util.Collections; 044 import java.util.Comparator; 045 import java.util.List; 046 047 import static org.kuali.rice.core.api.criteria.PredicateFactory.and; 048 import static org.kuali.rice.core.api.criteria.PredicateFactory.equal; 049 050 /** 051 * A node implementation which provides integration with KIM Roles for routing. 052 * Essentially extends RequestsNode and provides a custom RouteModule 053 * implementation. 054 * 055 * @author Kuali Rice Team (rice.collab@kuali.org) 056 * 057 */ 058 public class RoleNode extends RequestsNode { 059 060 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger 061 .getLogger( RoleNode.class ); 062 063 @Override 064 protected RouteModule getRouteModule(RouteContext context) throws Exception { 065 return new RoleRouteModule(); 066 } 067 068 /** 069 * @see org.kuali.rice.kew.engine.node.RequestsNode#processCustom(org.kuali.rice.kew.engine.RouteContext, org.kuali.rice.kew.engine.RouteHelper) 070 */ 071 @Override 072 protected boolean processCustom(RouteContext routeContext, RouteHelper routeHelper) throws Exception { 073 DocumentRouteHeaderValue document = routeContext.getDocument(); 074 RouteNodeInstance nodeInstance = routeContext.getNodeInstance(); 075 RouteNode node = nodeInstance.getRouteNode(); 076 // while no routable actions are activated and there are more 077 // routeLevels to process 078 if ( nodeInstance.isInitial() ) { 079 if ( LOG.isDebugEnabled() ) { 080 LOG.debug( "RouteHeader info inside routing loop\n" 081 + ClassDumper.dumpFields( routeContext.getDocument() ) ); 082 LOG.debug( "Looking for new actionRequests - routeLevel: " 083 + node.getRouteNodeName() ); 084 } 085 boolean suppressPolicyErrors = isSupressingPolicyErrors( routeContext ); 086 List<ActionRequestValue> requests = getNewActionRequests( routeContext ); 087 // Debugging code to force an empty action request 088 // if ( document.getDocumentType().getName().equals( "SACC" ) ) { 089 // LOG.fatal( "DEBUGGING CODE IN PLACE - SACC DOCUMENT ACTION REQUESTS CLEARED" ); 090 // requests.clear(); 091 // } 092 // for mandatory routes, requests must be generated 093 if ( requests.isEmpty() && !suppressPolicyErrors) { 094 Responsibility resp = getFirstResponsibilityWithMandatoryRouteFlag( document, node ); 095 if ( resp != null ) { 096 throw new RouteManagerException( "No requests generated for KIM Responsibility-based mandatory route.\n" + 097 "Document Id: " + document.getDocumentId() + "\n" + 098 "DocumentType: " + document.getDocumentType().getName() + "\n" + 099 "Route Node: " + node.getRouteNodeName() + "\n" + 100 "Responsibility: " + resp, 101 routeContext ); 102 } 103 } 104 // determine if we have any approve requests for FinalApprover 105 // checks 106 if ( !suppressPolicyErrors ) { 107 verifyFinalApprovalRequest( document, requests, nodeInstance, routeContext ); 108 } 109 } 110 return true; // to indicate custom processing performed 111 } 112 113 /** 114 * Checks for any mandatory route responsibilities for the given document type and node. 115 * 116 * Stops once it finds a responsibility for the document and node. 117 */ 118 protected Responsibility getFirstResponsibilityWithMandatoryRouteFlag( DocumentRouteHeaderValue document, RouteNode node ) { 119 // iterate over the document hierarchy 120 // gather responsibilities - merge based on route level 121 DocumentType docType = document.getDocumentType(); 122 while ( docType != null ) { 123 QueryByCriteria.Builder builder = QueryByCriteria.Builder.create(); 124 Predicate p = and( 125 equal("template.namespaceCode", KRADConstants.KUALI_RICE_WORKFLOW_NAMESPACE), 126 equal("template.name", KewApiConstants.DEFAULT_RESPONSIBILITY_TEMPLATE_NAME), 127 equal("active", "Y"), 128 equal("attributes[documentTypeName]", docType.getName()), 129 equal("attributes[routeNodeName]", node.getRouteNodeName()) 130 ); 131 builder.setPredicates(p); 132 133 List<Responsibility> responsibilities = KimApiServiceLocator.getResponsibilityService().findResponsibilities(builder.build()).getResults(); 134 // once we find a responsibility, stop, since this overrides any parent 135 // responsibilities for this node 136 if ( !responsibilities.isEmpty() ) { 137 // if any has required=true - return true 138 for ( Responsibility resp : responsibilities ) { 139 if ( Boolean.parseBoolean( resp.getAttributes().get( KimConstants.AttributeConstants.REQUIRED ) ) ) { 140 return resp; 141 } 142 } 143 return null; 144 } 145 docType = docType.getParentDocType(); 146 } 147 148 return null; 149 } 150 151 /** 152 * Activates the action requests that are pending at this routelevel of the 153 * document. The requests are processed by priority and then request ID. It 154 * is implicit in the access that the requests are activated according to 155 * the route level above all. 156 * <p> 157 * FYI and acknowledgment requests do not cause the processing to stop. Only 158 * action requests for approval or completion cause the processing to stop 159 * and then only for route level with a serialized activation policy. Only 160 * requests at the current document's current route level are activated. 161 * Inactive requests at a lower level cause a routing exception. 162 * <p> 163 * Exception routing and adhoc routing are processed slightly differently. 164 * 165 * @return True if the any approval actions were activated. 166 * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException 167 * @throws WorkflowException 168 */ 169 @SuppressWarnings("unchecked") 170 public boolean activateRequests(RouteContext context, DocumentRouteHeaderValue document, 171 RouteNodeInstance nodeInstance) throws WorkflowException { 172 MDC.put( "docId", document.getDocumentId() ); 173 PerformanceLogger performanceLogger = new PerformanceLogger( document.getDocumentId() ); 174 List<ActionItem> generatedActionItems = new ArrayList<ActionItem>(); 175 List<ActionRequestValue> requests = new ArrayList<ActionRequestValue>(); 176 if ( context.isSimulation() ) { 177 for ( ActionRequestValue ar : context.getDocument().getActionRequests() ) { 178 // logic check below duplicates behavior of the 179 // ActionRequestService.findPendingRootRequestsByDocIdAtRouteNode(documentId, 180 // routeNodeInstanceId) method 181 if ( ar.getCurrentIndicator() 182 && (ActionRequestStatus.INITIALIZED.getCode().equals( ar.getStatus() ) || ActionRequestStatus.ACTIVATED.getCode() 183 .equals( ar.getStatus() )) 184 && ar.getNodeInstance().getRouteNodeInstanceId().equals( 185 nodeInstance.getRouteNodeInstanceId() ) 186 && ar.getParentActionRequest() == null ) { 187 requests.add( ar ); 188 } 189 } 190 requests.addAll( context.getEngineState().getGeneratedRequests() ); 191 } else { 192 requests = KEWServiceLocator.getActionRequestService() 193 .findPendingRootRequestsByDocIdAtRouteNode( document.getDocumentId(), 194 nodeInstance.getRouteNodeInstanceId() ); 195 } 196 if ( LOG.isDebugEnabled() ) { 197 LOG.debug( "Pending Root Requests " + requests.size() ); 198 } 199 boolean requestActivated = activateRequestsCustom( context, requests, generatedActionItems, 200 document, nodeInstance ); 201 // now let's send notifications, since this code needs to be able to 202 // activate each request individually, we need 203 // to collection all action items and then notify after all have been 204 // generated 205 notify(context, generatedActionItems, nodeInstance); 206 207 performanceLogger.log( "Time to activate requests." ); 208 return requestActivated; 209 } 210 211 protected static class RoleRequestSorter implements Comparator<ActionRequestValue> { 212 public int compare(ActionRequestValue ar1, ActionRequestValue ar2) { 213 int result = 0; 214 // compare descriptions (only if both not null) 215 if ( ar1.getResponsibilityDesc() != null && ar2.getResponsibilityDesc() != null ) { 216 result = ar1.getResponsibilityDesc().compareTo( ar2.getResponsibilityDesc() ); 217 } 218 if ( result != 0 ) return result; 219 // compare priority 220 result = ar1.getPriority().compareTo(ar2.getPriority()); 221 if ( result != 0 ) return result; 222 // compare action request type 223 result = ActionRequestValue.compareActionCode(ar1.getActionRequested(), ar2.getActionRequested(), true); 224 if ( result != 0 ) return result; 225 // compare action request ID 226 if ( (ar1.getActionRequestId() != null) && (ar2.getActionRequestId() != null) ) { 227 result = ar1.getActionRequestId().compareTo(ar2.getActionRequestId()); 228 } else { 229 // if even one action request id is null at this point return then the two are equal 230 result = 0; 231 } 232 return result; 233 } 234 } 235 protected static final Comparator<ActionRequestValue> ROLE_REQUEST_SORTER = new RoleRequestSorter(); 236 237 238 protected boolean activateRequestsCustom(RouteContext context, 239 List<ActionRequestValue> requests, List<ActionItem> generatedActionItems, 240 DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance) 241 throws WorkflowException { 242 Collections.sort( requests, ROLE_REQUEST_SORTER ); 243 String activationType = nodeInstance.getRouteNode().getActivationType(); 244 boolean isParallel = KewApiConstants.ROUTE_LEVEL_PARALLEL.equals( activationType ); 245 boolean requestActivated = false; 246 String groupToActivate = null; 247 Integer priorityToActivate = null; 248 for ( ActionRequestValue request : requests ) { 249 // if a request has already been activated and we are not parallel routing 250 // or in the simulator, break out of the loop and exit 251 if ( requestActivated 252 && !isParallel 253 && (!context.isSimulation() || !context.getActivationContext() 254 .isActivateRequests()) ) { 255 break; 256 } 257 if ( request.getParentActionRequest() != null || request.getNodeInstance() == null ) { 258 // 1. disregard request if it's not a top-level request 259 // 2. disregard request if it's a "future" request and hasn't 260 // been attached to a node instance yet 261 continue; 262 } 263 if ( request.isApproveOrCompleteRequest() ) { 264 boolean thisRequestActivated = false; 265 // capture the priority and grouping information for this request 266 // We only need this for Approval requests since FYI and ACK requests are non-blocking 267 if ( priorityToActivate == null ) { 268 priorityToActivate = request.getPriority(); 269 } 270 if ( groupToActivate == null ) { 271 groupToActivate = request.getResponsibilityDesc(); 272 } 273 // check that the given request is found in the current group to activate 274 // check priority and grouping from the request (stored in the responsibility description) 275 if ( StringUtils.equals( groupToActivate, request.getResponsibilityDesc() ) 276 && ( 277 (priorityToActivate != null && request.getPriority() != null && priorityToActivate.equals(request.getPriority())) 278 || (priorityToActivate == null && request.getPriority() == null) 279 ) 280 ) { 281 // if the request is already active, note that we have an active request 282 // and move on to the next request 283 if ( request.isActive() ) { 284 requestActivated = true; 285 continue; 286 } 287 logProcessingMessage( request ); 288 if ( LOG.isDebugEnabled() ) { 289 LOG.debug( "Activating request: " + request ); 290 } 291 // this returns true if any requests were activated as a result of this call 292 thisRequestActivated = activateRequest( context, request, nodeInstance, 293 generatedActionItems ); 294 requestActivated |= thisRequestActivated; 295 } 296 // if this request was not activated and no request has been activated thus far 297 // then clear out the grouping and priority filters 298 // as this represents a case where the person with the earlier priority 299 // did not need to approve for this route level due to taking 300 // a prior action 301 if ( !thisRequestActivated && !requestActivated ) { 302 priorityToActivate = null; 303 groupToActivate = null; 304 } 305 } else { 306 logProcessingMessage( request ); 307 if ( LOG.isDebugEnabled() ) { 308 LOG.debug( "Activating request: " + request ); 309 } 310 requestActivated = activateRequest( context, request, nodeInstance, 311 generatedActionItems ) 312 || requestActivated; 313 } 314 } 315 return requestActivated; 316 } 317 }