001 /** 002 * Copyright 2005-2014 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 = isSuppressingPolicyErrors(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", Boolean.TRUE), 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 protected static class RoleRequestSorter implements Comparator<ActionRequestValue> { 156 public int compare(ActionRequestValue ar1, ActionRequestValue ar2) { 157 int result = 0; 158 // compare descriptions (only if both not null) 159 if ( ar1.getResponsibilityDesc() != null && ar2.getResponsibilityDesc() != null ) { 160 result = ar1.getResponsibilityDesc().compareTo( ar2.getResponsibilityDesc() ); 161 } 162 if ( result != 0 ) return result; 163 // compare priority 164 result = ar1.getPriority().compareTo(ar2.getPriority()); 165 if ( result != 0 ) return result; 166 // compare action request type 167 result = ActionRequestValue.compareActionCode(ar1.getActionRequested(), ar2.getActionRequested(), true); 168 if ( result != 0 ) return result; 169 // compare action request ID 170 if ( (ar1.getActionRequestId() != null) && (ar2.getActionRequestId() != null) ) { 171 result = ar1.getActionRequestId().compareTo(ar2.getActionRequestId()); 172 } else { 173 // if even one action request id is null at this point return then the two are equal 174 result = 0; 175 } 176 return result; 177 } 178 } 179 protected static final Comparator<ActionRequestValue> ROLE_REQUEST_SORTER = new RoleRequestSorter(); 180 181 182 protected boolean activateRequestsCustom(RouteContext context, 183 List<ActionRequestValue> requests, List<ActionItem> generatedActionItems, 184 DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance) 185 throws WorkflowException { 186 // copy the list so's we can sort it 187 requests = new ArrayList<ActionRequestValue>(requests); 188 Collections.sort( requests, ROLE_REQUEST_SORTER ); 189 String activationType = nodeInstance.getRouteNode().getActivationType(); 190 boolean isParallel = KewApiConstants.ROUTE_LEVEL_PARALLEL.equals( activationType ); 191 boolean requestActivated = false; 192 String groupToActivate = null; 193 Integer priorityToActivate = null; 194 for ( ActionRequestValue request : requests ) { 195 // if a request has already been activated and we are not parallel routing 196 // or in the simulator, break out of the loop and exit 197 if ( requestActivated 198 && !isParallel 199 && (!context.isSimulation() || !context.getActivationContext() 200 .isActivateRequests()) ) { 201 break; 202 } 203 if ( request.getParentActionRequest() != null || request.getNodeInstance() == null ) { 204 // 1. disregard request if it's not a top-level request 205 // 2. disregard request if it's a "future" request and hasn't 206 // been attached to a node instance yet 207 continue; 208 } 209 if ( request.isApproveOrCompleteRequest() ) { 210 boolean thisRequestActivated = false; 211 // capture the priority and grouping information for this request 212 // We only need this for Approval requests since FYI and ACK requests are non-blocking 213 if ( priorityToActivate == null ) { 214 priorityToActivate = request.getPriority(); 215 } 216 if ( groupToActivate == null ) { 217 groupToActivate = request.getResponsibilityDesc(); 218 } 219 // check that the given request is found in the current group to activate 220 // check priority and grouping from the request (stored in the responsibility description) 221 if ( StringUtils.equals( groupToActivate, request.getResponsibilityDesc() ) 222 && ( 223 (priorityToActivate != null && request.getPriority() != null && priorityToActivate.equals(request.getPriority())) 224 || (priorityToActivate == null && request.getPriority() == null) 225 ) 226 ) { 227 // if the request is already active, note that we have an active request 228 // and move on to the next request 229 if ( request.isActive() ) { 230 requestActivated = true; 231 continue; 232 } 233 logProcessingMessage( request ); 234 if ( LOG.isDebugEnabled() ) { 235 LOG.debug( "Activating request: " + request ); 236 } 237 // this returns true if any requests were activated as a result of this call 238 thisRequestActivated = activateRequest( context, request, nodeInstance, 239 generatedActionItems ); 240 requestActivated |= thisRequestActivated; 241 } 242 // if this request was not activated and no request has been activated thus far 243 // then clear out the grouping and priority filters 244 // as this represents a case where the person with the earlier priority 245 // did not need to approve for this route level due to taking 246 // a prior action 247 if ( !thisRequestActivated && !requestActivated ) { 248 priorityToActivate = null; 249 groupToActivate = null; 250 } 251 } else { 252 logProcessingMessage( request ); 253 if ( LOG.isDebugEnabled() ) { 254 LOG.debug( "Activating request: " + request ); 255 } 256 requestActivated = activateRequest( context, request, nodeInstance, 257 generatedActionItems ) 258 || requestActivated; 259 } 260 } 261 return requestActivated; 262 } 263 }