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    }