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