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    }