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 */
016package org.kuali.rice.kew.engine.node;
017
018import java.util.ArrayList;
019import java.util.HashSet;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Set;
023
024import org.apache.commons.collections.CollectionUtils;
025import org.apache.commons.lang.ObjectUtils;
026import org.kuali.rice.coreservice.framework.parameter.ParameterService;
027import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
028import org.kuali.rice.kew.actionrequest.ActionRequestValue;
029import org.kuali.rice.kew.engine.RouteContext;
030import org.kuali.rice.kew.engine.RouteHelper;
031import org.kuali.rice.kew.exception.RouteManagerException;
032import org.kuali.rice.kew.api.exception.WorkflowException;
033import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
034import org.kuali.rice.kew.routemodule.RouteModule;
035import org.kuali.rice.kew.service.KEWServiceLocator;
036import org.kuali.rice.kew.util.ClassDumper;
037import org.kuali.rice.kew.api.KewApiConstants;
038import org.kuali.rice.krad.util.KRADConstants;
039
040/**
041 * A node which generates {@link ActionRequestValue} objects from a
042 * {@link RouteModule}.
043 *
044 * @author Kuali Rice Team (rice.collab@kuali.org)
045 */
046public class RequestsNode extends RequestActivationNode {
047
048        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
049                        .getLogger( RequestsNode.class );
050
051        protected static final String SUPPRESS_POLICY_ERRORS_KEY = "_suppressPolicyErrorsRequestActivationNode";
052
053        public final SimpleResult process(RouteContext routeContext, RouteHelper routeHelper)
054                        throws Exception {
055                try {
056            if (processCustom(routeContext, routeHelper)) {
057                return super.process(routeContext, routeHelper);
058            }
059            RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
060            boolean isInitial = nodeInstance.isInitial();
061            int currentIteration = 0;
062            List<ActionRequestValue> requestsGenerated = new ArrayList<ActionRequestValue>();
063            while (true) {
064                detectRunawayProcess(routeContext, currentIteration++);
065                if (isInitial) {
066                    requestsGenerated = generateRequests(routeContext);
067                    // need to set to false because we could iterate more than once here when the node is still in
068                    // "initial" state
069                    isInitial = false;
070                }
071                SimpleResult simpleResult = super.process(routeContext, routeHelper);
072                if (simpleResult.isComplete()) {
073                    RouteModule routeModule = getRouteModule(routeContext);
074                    boolean moreRequestsAvailable = routeModule.isMoreRequestsAvailable(routeContext);
075                    if (!moreRequestsAvailable) {
076                        applyPoliciesOnExit(requestsGenerated, routeContext);
077                        return simpleResult;
078                    } else {
079                        requestsGenerated = generateRequests(routeContext);
080                    }
081                } else {
082                    return simpleResult;
083                }
084            }
085                } catch ( RouteManagerException ex ) {
086                        // just re-throw - no need to wrap
087                        throw ex;
088                } catch ( Exception e ) {
089                        LOG.error( "Caught exception routing", e );
090                        throw new RouteManagerException( e.getMessage(), e, routeContext );
091                }
092        }
093
094    protected List<ActionRequestValue> generateRequests(RouteContext routeContext) throws Exception {
095        DocumentRouteHeaderValue document = routeContext.getDocument();
096                RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
097                RouteNode node = nodeInstance.getRouteNode();
098        if (LOG.isDebugEnabled()) {
099            LOG.debug("RouteHeader info inside routing loop\n" + ClassDumper.dumpFields(document));
100            LOG.debug("Looking for new actionRequests - routeLevel: " + node.getRouteNodeName());
101        }
102        boolean suppressPolicyErrors = isSuppressingPolicyErrors(routeContext);
103        List<ActionRequestValue> requests = getNewActionRequests(routeContext);
104        // determine if we have any approve requests for FinalApprover checks
105        if (!suppressPolicyErrors) {
106            verifyFinalApprovalRequest(document, requests, nodeInstance, routeContext);
107        }
108        return requests;
109    }
110
111    /**
112     * Applies policies that should get checked prior to transitioning out of this node.  The default implementation of
113     * this method checks the "mandatory" policy.
114     *
115     * @param requestsGenerated the requests generated on the current iteration of the route module
116     * @param routeContext the current route context
117     */
118    protected void applyPoliciesOnExit(List<ActionRequestValue> requestsGenerated, RouteContext routeContext) {
119        DocumentRouteHeaderValue document = routeContext.getDocument();
120        RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
121        RouteNode node = nodeInstance.getRouteNode();
122        // for mandatory routes, requests must be generated
123        if (node.isMandatory() && !isSuppressingPolicyErrors(routeContext) && CollectionUtils.isEmpty(requestsGenerated)) {
124            List<ActionRequestValue> actionRequests = KEWServiceLocator.getActionRequestService().findRootRequestsByDocIdAtRouteNode(document.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
125            if (actionRequests.isEmpty()) {
126                LOG.warn("no requests generated for mandatory route - " + node.getRouteNodeName());
127                throw new RouteManagerException(
128                    "No requests generated for mandatory route " + node.getRouteNodeName() + ":" + node
129                            .getRouteMethodName(), routeContext);
130            }
131        }
132    }
133
134    /** Used by subclasses to replace the functioning of the process method.
135         *
136         * @return <b>true</b> if custom processing was performed and the base implementation
137         * in {@link #process(RouteContext, RouteHelper)} should be skipped.
138         */
139        protected boolean processCustom(RouteContext routeContext, RouteHelper routeHelper) throws Exception {
140                return false;
141        }
142
143        /**
144         * Verifies the state of the action requests when a final approval action is involved.
145         *
146         * Throws a RouteManagerException if actions were not generated correctly.
147         */
148        protected void verifyFinalApprovalRequest( DocumentRouteHeaderValue document, List<ActionRequestValue> requests, RouteNodeInstance nodeInstance, RouteContext routeContext ) throws RouteManagerException {
149                boolean pastFinalApprover = isPastFinalApprover( document, nodeInstance );
150                boolean hasApproveRequest = false;
151                for ( ActionRequestValue actionRequest : requests ) {
152                        if ( actionRequest.isApproveOrCompleteRequest() ) {
153                                hasApproveRequest = true;
154                                break;
155                        }
156                }
157                // if final approver route level and no approve request send to
158                // exception routing
159                if ( nodeInstance.getRouteNode().getFinalApprovalInd().booleanValue() ) {
160                        // we must have an approve request generated if final
161                        // approver level.
162                        if ( !hasApproveRequest ) {
163                                throw new RouteManagerException(
164                                                "No Approve Request generated after final approver", routeContext );
165                        }
166                } else if ( pastFinalApprover ) {
167                        // we can't allow generation of approve requests after final
168                        // approver. This guys going to exception routing.
169                        if ( hasApproveRequest ) {
170                                throw new RouteManagerException(
171                                                "Approve Request generated after final approver", routeContext );
172                        }
173                }
174        }
175
176        public List<ActionRequestValue> getNewActionRequests(RouteContext context) throws Exception {
177                RouteNodeInstance nodeInstance = context.getNodeInstance();
178                String routeMethodName = nodeInstance.getRouteNode().getRouteMethodName();
179                if ( LOG.isDebugEnabled() ) {
180                        LOG.debug( "Looking for action requests in " + routeMethodName + " : "
181                                        + nodeInstance.getRouteNode().getRouteNodeName() );
182                }
183                List<ActionRequestValue> newRequests = new ArrayList<ActionRequestValue>();
184                try {
185                        RouteModule routeModule = getRouteModule( context );
186                        List<ActionRequestValue> requests = routeModule.findActionRequests( context );
187            // route module should only be returning root requests to us, but in case it doesn't...
188            requests = KEWServiceLocator.getActionRequestService().getRootRequests(requests);
189            List<ActionRequestValue> uniqueRequests = new ArrayList<ActionRequestValue>();
190            for ( ActionRequestValue actionRequest : requests ) {
191                boolean duplicateFound = false;
192                for (ActionRequestValue uniqueRequest: uniqueRequests ) {
193                    if (isDuplicateActionRequestDetected(uniqueRequest, actionRequest)) {
194                        duplicateFound = true;
195                        break;
196                    }
197                }
198                if (!duplicateFound) {
199                    uniqueRequests.add(actionRequest);
200                    duplicateFound = false;
201                }
202            }
203            for ( ActionRequestValue actionRequest : uniqueRequests ) {
204                                if ( LOG.isDebugEnabled() ) {
205                                        LOG.debug( "Request generated by RouteModule '" + routeModule + "' for node "
206                                                        + nodeInstance + ":" + actionRequest );
207                                }
208                                actionRequest = KEWServiceLocator.getActionRequestService()
209                                                .initializeActionRequestGraph( actionRequest, context.getDocument(),
210                                                                nodeInstance );
211                                actionRequest = saveActionRequest( context, actionRequest );
212                                newRequests.add( actionRequest );
213                        }
214                } catch ( WorkflowException ex ) {
215                        LOG.warn( "Caught WorkflowException during routing", ex );
216                        throw new RouteManagerException( ex, context );
217                }
218                return newRequests;
219        }
220    private boolean isDuplicateActionRequestDetected(ActionRequestValue actionRequest, ActionRequestValue actionRequestToCompare) {
221        if ( (ObjectUtils.equals(actionRequest.getActionRequested(), actionRequestToCompare.getActionRequested())) &&
222                (ObjectUtils.equals(actionRequest.getPrincipalId(), actionRequestToCompare.getPrincipalId())) &&
223                (ObjectUtils.equals(actionRequest.getStatus(), actionRequestToCompare.getStatus())) &&
224                (ObjectUtils.equals(actionRequest.getResponsibilityId(), actionRequestToCompare.getResponsibilityId())) &&
225                (ObjectUtils.equals(actionRequest.getGroupId(), actionRequestToCompare.getGroupId())) &&
226                (ObjectUtils.equals(actionRequest.getPriority(), actionRequestToCompare.getPriority())) &&
227                (ObjectUtils.equals(actionRequest.getRouteLevel(), actionRequestToCompare.getRouteLevel())) &&
228                (ObjectUtils.equals(actionRequest.getResponsibilityDesc(), actionRequestToCompare.getResponsibilityDesc())) &&
229                (ObjectUtils.equals(actionRequest.getAnnotation(), actionRequestToCompare.getAnnotation())) &&
230                (ObjectUtils.equals(actionRequest.getForceAction(), actionRequestToCompare.getForceAction())) &&
231                (ObjectUtils.equals(actionRequest.getQualifiedRoleName(), actionRequestToCompare.getQualifiedRoleName())) &&
232                (ObjectUtils.equals(actionRequest.getRoleName(), actionRequestToCompare.getRoleName())) &&
233                (ObjectUtils.equals(actionRequest.getApprovePolicy(), actionRequestToCompare.getApprovePolicy())) &&
234                (ObjectUtils.equals(actionRequest.getCurrentIndicator(), actionRequestToCompare.getCurrentIndicator())) &&
235                (ObjectUtils.equals(actionRequest.getNodeInstance(), actionRequestToCompare.getNodeInstance())) &&
236                (ObjectUtils.equals(actionRequest.getActionTaken(), actionRequestToCompare.getActionTaken())) &&
237                (ObjectUtils.equals(actionRequest.getDelegationType(), actionRequestToCompare.getDelegationType())) &&
238                (ObjectUtils.equals(actionRequest.getRuleBaseValuesId(), actionRequestToCompare.getRuleBaseValuesId())) &&
239                (ObjectUtils.equals(actionRequest.getDisplayStatus(), actionRequestToCompare.getDisplayStatus())) &&
240                (ObjectUtils.equals(actionRequest.getQualifiedRoleNameLabel(), actionRequestToCompare.getQualifiedRoleNameLabel())) &&
241                (ObjectUtils.equals(actionRequest.getParentActionRequestId(), actionRequestToCompare.getParentActionRequestId())) &&
242                (ObjectUtils.equals(actionRequest.getDocVersion(), actionRequestToCompare.getDocVersion())) &&
243                (ObjectUtils.equals(actionRequest.getActionTakenId(), actionRequestToCompare.getActionTakenId())) &&
244                (ObjectUtils.equals(actionRequest.getDocumentId(), actionRequestToCompare.getDocumentId())) ) {
245            return true;
246        } else {
247            return false;
248        }
249    }
250
251    /**
252         * Returns the RouteModule which should handle generating requests for this
253         * RequestsNode.
254         */
255        protected RouteModule getRouteModule(RouteContext context) throws Exception {
256                return KEWServiceLocator.getRouteModuleService().findRouteModule(
257                                context.getNodeInstance().getRouteNode() );
258        }
259
260        /**
261         * Checks if the document has past the final approver node by walking
262         * backward through the previous node instances. Ignores any previous nodes
263         * that have been "revoked".
264         */
265        protected boolean isPastFinalApprover(DocumentRouteHeaderValue document,
266                        RouteNodeInstance nodeInstance) {
267                FinalApproverContext context = new FinalApproverContext();
268                List revokedNodeInstances = KEWServiceLocator.getRouteNodeService()
269                                .getRevokedNodeInstances( document );
270                Set revokedNodeInstanceIds = new HashSet();
271                for ( Iterator iterator = revokedNodeInstances.iterator(); iterator.hasNext(); ) {
272                        RouteNodeInstance revokedNodeInstance = (RouteNodeInstance)iterator.next();
273                        revokedNodeInstanceIds.add( revokedNodeInstance.getRouteNodeInstanceId() );
274                }
275                isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
276                                revokedNodeInstanceIds );
277                return context.isPast;
278        }
279
280        protected void isPastFinalApprover(List previousNodeInstances, FinalApproverContext context,
281                        Set revokedNodeInstanceIds) {
282                if ( previousNodeInstances != null && !previousNodeInstances.isEmpty() ) {
283                        for ( Iterator iterator = previousNodeInstances.iterator(); iterator.hasNext(); ) {
284                                if ( context.isPast ) {
285                                        return;
286                                }
287                                RouteNodeInstance nodeInstance = (RouteNodeInstance)iterator.next();
288                                if ( context.inspected.contains( getKey( nodeInstance ) ) ) {
289                                        continue;
290                                } else {
291                                        context.inspected.add( getKey( nodeInstance ) );
292                                }
293                                if ( Boolean.TRUE.equals( nodeInstance.getRouteNode().getFinalApprovalInd() ) ) {
294                                        // if the node instance has been revoked (by a Return To
295                                        // Previous action for example)
296                                        // then we don't want to consider that node when we
297                                        // determine if we are past final
298                                        // approval or not
299                                        if ( !revokedNodeInstanceIds.contains( nodeInstance.getRouteNodeInstanceId() ) ) {
300                                                context.isPast = true;
301                                        }
302                                        return;
303                                }
304                                isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
305                                                revokedNodeInstanceIds );
306                        }
307                }
308        }
309
310        /**
311         * The method will get a key value which can be used for comparison
312         * purposes. If the node instance has a primary key value, it will be
313         * returned. However, if the node instance has not been saved to the
314         * database (i.e. during a simulation) this method will return the node
315         * instance passed in.
316         */
317        protected Object getKey(RouteNodeInstance nodeInstance) {
318                String id = nodeInstance.getRouteNodeInstanceId();
319                return (id != null ? (Object)id : (Object)nodeInstance);
320        }
321
322    protected void detectRunawayProcess(RouteContext routeContext, int currentIteration) throws NumberFormatException {
323            String maxNodesConstant = getParameterService().getParameterValueAsString(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.MAX_NODES_BEFORE_RUNAWAY_PROCESS);
324            int maxNodes = (org.apache.commons.lang.StringUtils.isEmpty(maxNodesConstant)) ? 50 : Integer.valueOf(maxNodesConstant);
325            if (currentIteration > maxNodes) {
326            throw new RouteManagerException("Detected a runaway process within RequestsNode for document with id '" + routeContext.getDocument().getDocumentId() + "' after " + currentIteration + " iterations.");
327        }
328        }
329
330        protected class FinalApproverContext {
331                public Set inspected = new HashSet();
332
333                public boolean isPast = false;
334        }
335
336        public static boolean isSuppressingPolicyErrors(RouteContext routeContext) {
337                Boolean suppressPolicyErrors = (Boolean)routeContext.getParameters().get(
338                                SUPPRESS_POLICY_ERRORS_KEY );
339                if ( suppressPolicyErrors == null || !suppressPolicyErrors ) {
340                        return false;
341                }
342                return true;
343        }
344
345        @SuppressWarnings("unchecked")
346        public static void setSuppressPolicyErrors(RouteContext routeContext) {
347                routeContext.getParameters().put( SUPPRESS_POLICY_ERRORS_KEY, Boolean.TRUE );
348        }
349
350    protected ParameterService getParameterService() {
351                return CoreFrameworkServiceLocator.getParameterService();
352        }
353}