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 }