001/** 002 * Copyright 2005-2016 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.node; 017 018import org.apache.commons.lang.StringUtils; 019import org.apache.log4j.MDC; 020import org.kuali.rice.core.api.criteria.Predicate; 021import org.kuali.rice.core.api.criteria.QueryByCriteria; 022import org.kuali.rice.kew.actionitem.ActionItem; 023import org.kuali.rice.kew.actionrequest.ActionRequestValue; 024import org.kuali.rice.kew.api.action.ActionRequestStatus; 025import org.kuali.rice.kew.doctype.bo.DocumentType; 026import org.kuali.rice.kew.engine.RouteContext; 027import org.kuali.rice.kew.engine.RouteHelper; 028import org.kuali.rice.kew.exception.RouteManagerException; 029import org.kuali.rice.kew.api.exception.WorkflowException; 030import org.kuali.rice.kew.role.RoleRouteModule; 031import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 032import org.kuali.rice.kew.routemodule.RouteModule; 033import org.kuali.rice.kew.service.KEWServiceLocator; 034import org.kuali.rice.kew.util.ClassDumper; 035import org.kuali.rice.kew.api.KewApiConstants; 036import org.kuali.rice.kew.util.PerformanceLogger; 037import org.kuali.rice.kim.api.KimConstants; 038import org.kuali.rice.kim.api.responsibility.Responsibility; 039import org.kuali.rice.kim.api.services.KimApiServiceLocator; 040import org.kuali.rice.krad.util.KRADConstants; 041 042import java.util.ArrayList; 043import java.util.Collections; 044import java.util.Comparator; 045import java.util.List; 046 047import static org.kuali.rice.core.api.criteria.PredicateFactory.and; 048import 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 */ 058public 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 Predicate p = and( 122 equal("template.namespaceCode", KRADConstants.KUALI_RICE_WORKFLOW_NAMESPACE), 123 equal("template.name", KewApiConstants.DEFAULT_RESPONSIBILITY_TEMPLATE_NAME), 124 equal("active", "Y"), 125 equal("attributes[routeNodeName]", node.getRouteNodeName()) 126 // KULRICE-8538 -- Check the document type while we're looping through the results below. If it is added 127 // into the predicate, no rows are ever returned. 128 // equal("attributes[documentTypeName]", docType.getName()) 129 ); 130 QueryByCriteria.Builder builder = QueryByCriteria.Builder.create(); 131 builder.setPredicates(p); 132 List<Responsibility> responsibilities = KimApiServiceLocator.getResponsibilityService().findResponsibilities(builder.build()).getResults(); 133 134 135 DocumentType docType = document.getDocumentType(); 136 while ( docType != null ) { 137 // once we find a responsibility, stop, since this overrides any parent 138 // responsibilities for this node 139 if ( !responsibilities.isEmpty() ) { 140 // if any has required=true - return true 141 for ( Responsibility resp : responsibilities ) { 142 String documentTypeName = resp.getAttributes().get( KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME); 143 if (StringUtils.isNotEmpty(documentTypeName) && StringUtils.equals(documentTypeName, docType.getName())){ 144 if ( Boolean.parseBoolean( resp.getAttributes().get( KimConstants.AttributeConstants.REQUIRED ) ) ) { 145 return resp; 146 } 147 } 148 } 149 } 150 docType = docType.getParentDocType(); 151 } 152 return null; 153 } 154 155 /** 156 * Activates the action requests that are pending at this routelevel of the 157 * document. The requests are processed by priority and then request ID. It 158 * is implicit in the access that the requests are activated according to 159 * the route level above all. 160 * <p> 161 * FYI and acknowledgment requests do not cause the processing to stop. Only 162 * action requests for approval or completion cause the processing to stop 163 * and then only for route level with a serialized activation policy. Only 164 * requests at the current document's current route level are activated. 165 * Inactive requests at a lower level cause a routing exception. 166 * <p> 167 * Exception routing and adhoc routing are processed slightly differently. 168 * 169 * @return True if the any approval actions were activated. 170 * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException 171 * @throws WorkflowException 172 */ 173 @SuppressWarnings("unchecked") 174 public boolean activateRequests(RouteContext context, DocumentRouteHeaderValue document, 175 RouteNodeInstance nodeInstance) throws WorkflowException { 176 MDC.put( "docId", document.getDocumentId() ); 177 PerformanceLogger performanceLogger = new PerformanceLogger( document.getDocumentId() ); 178 List<ActionItem> generatedActionItems = new ArrayList<ActionItem>(); 179 List<ActionRequestValue> requests = new ArrayList<ActionRequestValue>(); 180 if ( context.isSimulation() ) { 181 for ( ActionRequestValue ar : context.getDocument().getActionRequests() ) { 182 // logic check below duplicates behavior of the 183 // ActionRequestService.findPendingRootRequestsByDocIdAtRouteNode(documentId, 184 // routeNodeInstanceId) method 185 if ( ar.getCurrentIndicator() 186 && (ActionRequestStatus.INITIALIZED.getCode().equals( ar.getStatus() ) || ActionRequestStatus.ACTIVATED.getCode() 187 .equals( ar.getStatus() )) 188 && ar.getNodeInstance().getRouteNodeInstanceId().equals( 189 nodeInstance.getRouteNodeInstanceId() ) 190 && ar.getParentActionRequest() == null ) { 191 requests.add( ar ); 192 } 193 } 194 requests.addAll( context.getEngineState().getGeneratedRequests() ); 195 } else { 196 requests = KEWServiceLocator.getActionRequestService() 197 .findPendingRootRequestsByDocIdAtRouteNode( document.getDocumentId(), 198 nodeInstance.getRouteNodeInstanceId() ); 199 } 200 if ( LOG.isDebugEnabled() ) { 201 LOG.debug( "Pending Root Requests " + requests.size() ); 202 } 203 boolean requestActivated = activateRequestsCustom( context, requests, generatedActionItems, 204 document, nodeInstance ); 205 // now let's send notifications, since this code needs to be able to 206 // activate each request individually, we need 207 // to collection all action items and then notify after all have been 208 // generated 209 notify(context, generatedActionItems, nodeInstance); 210 211 performanceLogger.log( "Time to activate requests." ); 212 return requestActivated; 213 } 214 215 protected static class RoleRequestSorter implements Comparator<ActionRequestValue> { 216 public int compare(ActionRequestValue ar1, ActionRequestValue ar2) { 217 int result = 0; 218 // compare descriptions (only if both not null) 219 if ( ar1.getResponsibilityDesc() != null && ar2.getResponsibilityDesc() != null ) { 220 result = ar1.getResponsibilityDesc().compareTo( ar2.getResponsibilityDesc() ); 221 } 222 if ( result != 0 ) return result; 223 // compare priority 224 result = ar1.getPriority().compareTo(ar2.getPriority()); 225 if ( result != 0 ) return result; 226 // compare action request type 227 result = ActionRequestValue.compareActionCode(ar1.getActionRequested(), ar2.getActionRequested(), true); 228 if ( result != 0 ) return result; 229 // compare action request ID 230 if ( (ar1.getActionRequestId() != null) && (ar2.getActionRequestId() != null) ) { 231 result = ar1.getActionRequestId().compareTo(ar2.getActionRequestId()); 232 } else { 233 // if even one action request id is null at this point return then the two are equal 234 result = 0; 235 } 236 return result; 237 } 238 } 239 protected static final Comparator<ActionRequestValue> ROLE_REQUEST_SORTER = new RoleRequestSorter(); 240 241 242 protected boolean activateRequestsCustom(RouteContext context, 243 List<ActionRequestValue> requests, List<ActionItem> generatedActionItems, 244 DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance) 245 throws WorkflowException { 246 Collections.sort( requests, ROLE_REQUEST_SORTER ); 247 String activationType = nodeInstance.getRouteNode().getActivationType(); 248 boolean isParallel = KewApiConstants.ROUTE_LEVEL_PARALLEL.equals( activationType ); 249 boolean requestActivated = false; 250 String groupToActivate = null; 251 Integer priorityToActivate = null; 252 for ( ActionRequestValue request : requests ) { 253 // if a request has already been activated and we are not parallel routing 254 // or in the simulator, break out of the loop and exit 255 if ( requestActivated 256 && !isParallel 257 && (!context.isSimulation() || !context.getActivationContext() 258 .isActivateRequests()) ) { 259 break; 260 } 261 if ( request.getParentActionRequest() != null || request.getNodeInstance() == null ) { 262 // 1. disregard request if it's not a top-level request 263 // 2. disregard request if it's a "future" request and hasn't 264 // been attached to a node instance yet 265 continue; 266 } 267 if ( request.isApproveOrCompleteRequest() ) { 268 boolean thisRequestActivated = false; 269 // capture the priority and grouping information for this request 270 // We only need this for Approval requests since FYI and ACK requests are non-blocking 271 if ( priorityToActivate == null ) { 272 priorityToActivate = request.getPriority(); 273 } 274 if ( groupToActivate == null ) { 275 groupToActivate = request.getResponsibilityDesc(); 276 } 277 // check that the given request is found in the current group to activate 278 // check priority and grouping from the request (stored in the responsibility description) 279 if ( StringUtils.equals( groupToActivate, request.getResponsibilityDesc() ) 280 && ( 281 (priorityToActivate != null && request.getPriority() != null && priorityToActivate.equals(request.getPriority())) 282 || (priorityToActivate == null && request.getPriority() == null) 283 ) 284 ) { 285 // if the request is already active, note that we have an active request 286 // and move on to the next request 287 if ( request.isActive() ) { 288 requestActivated = true; 289 continue; 290 } 291 logProcessingMessage( request ); 292 if ( LOG.isDebugEnabled() ) { 293 LOG.debug( "Activating request: " + request ); 294 } 295 // this returns true if any requests were activated as a result of this call 296 thisRequestActivated = activateRequest( context, request, nodeInstance, 297 generatedActionItems ); 298 requestActivated |= thisRequestActivated; 299 } 300 // if this request was not activated and no request has been activated thus far 301 // then clear out the grouping and priority filters 302 // as this represents a case where the person with the earlier priority 303 // did not need to approve for this route level due to taking 304 // a prior action 305 if ( !thisRequestActivated && !requestActivated ) { 306 priorityToActivate = null; 307 groupToActivate = null; 308 } 309 } else { 310 logProcessingMessage( request ); 311 if ( LOG.isDebugEnabled() ) { 312 LOG.debug( "Activating request: " + request ); 313 } 314 requestActivated = activateRequest( context, request, nodeInstance, 315 generatedActionItems ) 316 || requestActivated; 317 } 318 } 319 return requestActivated; 320 } 321}