001    /**
002     * Copyright 2005-2012 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 java.util.ArrayList;
019    import java.util.HashSet;
020    import java.util.Iterator;
021    import java.util.List;
022    import java.util.Set;
023    
024    import org.apache.commons.collections.CollectionUtils;
025    import org.kuali.rice.coreservice.framework.parameter.ParameterService;
026    import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
027    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
028    import org.kuali.rice.kew.engine.RouteContext;
029    import org.kuali.rice.kew.engine.RouteHelper;
030    import org.kuali.rice.kew.exception.RouteManagerException;
031    import org.kuali.rice.kew.api.exception.WorkflowException;
032    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
033    import org.kuali.rice.kew.routemodule.RouteModule;
034    import org.kuali.rice.kew.service.KEWServiceLocator;
035    import org.kuali.rice.kew.util.ClassDumper;
036    import org.kuali.rice.kew.api.KewApiConstants;
037    import 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     */
045    public 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 = isSupressingPolicyErrors(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() && !isSupressingPolicyErrors(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 isSupressingPolicyErrors(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 setSupressPolicyErrors(RouteContext routeContext) {
300                    routeContext.getParameters().put( SUPPRESS_POLICY_ERRORS_KEY, Boolean.TRUE );
301            }
302    
303        protected ParameterService getParameterService() {
304                    return CoreFrameworkServiceLocator.getParameterService();
305            }
306    }