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}