001    /*
002     * Copyright 2005-2007 The Kuali Foundation
003     * 
004     * 
005     * Licensed under the Educational Community License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     * 
009     * http://www.opensource.org/licenses/ecl2.php
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.kuali.rice.kew.engine.node;
018    
019    import java.util.ArrayList;
020    import java.util.HashSet;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Set;
024    
025    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
026    import org.kuali.rice.kew.engine.RouteContext;
027    import org.kuali.rice.kew.engine.RouteHelper;
028    import org.kuali.rice.kew.exception.ResourceUnavailableException;
029    import org.kuali.rice.kew.exception.RouteManagerException;
030    import org.kuali.rice.kew.exception.WorkflowException;
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    
036    /**
037     * A node which generates {@link ActionRequestValue} objects from a
038     * {@link RouteModule}.
039     * 
040     * @author Kuali Rice Team (rice.collab@kuali.org)
041     */
042    public class RequestsNode extends RequestActivationNode {
043    
044            private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
045                            .getLogger( RequestsNode.class );
046    
047            protected static final String SUPPRESS_POLICY_ERRORS_KEY = "_suppressPolicyErrorsRequestActivationNode";
048    
049            public final SimpleResult process(RouteContext routeContext, RouteHelper routeHelper)
050                            throws Exception {
051                    try {
052                            if ( !processCustom( routeContext, routeHelper ) ) {
053                                    DocumentRouteHeaderValue document = routeContext.getDocument();
054                                    RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
055                                    RouteNode node = nodeInstance.getRouteNode();
056                                    // refreshSearchableAttributes(routeContext);
057                                    // while no routable actions are activated and there are more
058                                    // routeLevels to process
059                                    if ( nodeInstance.isInitial() ) {
060                                            if ( LOG.isDebugEnabled() ) {
061                                                    LOG.debug( "RouteHeader info inside routing loop\n"
062                                                                    + ClassDumper.dumpFields( document ) );
063                                                    LOG.debug( "Looking for new actionRequests - routeLevel: "
064                                                                    + node.getRouteNodeName() );
065                                            }
066                                            boolean suppressPolicyErrors = isSupressingPolicyErrors( routeContext );
067                                            List<ActionRequestValue> requests = getNewActionRequests( routeContext );
068                                            // for mandatory routes, requests must be generated
069                                            if ( (requests.isEmpty()) && node.getMandatoryRouteInd().booleanValue()
070                                                            && !suppressPolicyErrors ) {
071                                                    LOG.warn( "no requests generated for mandatory route - "
072                                                                    + node.getRouteNodeName() );
073                                                    throw new RouteManagerException( "No requests generated for mandatory route "
074                                                                    + node.getRouteNodeName() + ":" + node.getRouteMethodName(),
075                                                                    routeContext );
076                                            }
077                                            // determine if we have any approve requests for FinalApprover
078                                            // checks
079                                            if ( !suppressPolicyErrors ) {                          
080                                                    verifyFinalApprovalRequest( document, requests, nodeInstance, routeContext );
081                                            }
082                                    }
083                            }
084                            return super.process( routeContext, routeHelper );
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            /** Used by subclasses to replace the functioning of the process method.
095             * 
096             * @return <b>true</b> if custom processing was performed and the base implementation
097             * in {@link #process(RouteContext, RouteHelper)} should be skipped.
098             */
099            protected boolean processCustom(RouteContext routeContext, RouteHelper routeHelper) throws Exception {
100                    return false;
101            }
102            
103            /**
104             * Verifies the state of the action requests when a final approval action is involved.
105             * 
106             * Throws a RouteManagerException if actions were not generated correctly.
107             */
108            protected void verifyFinalApprovalRequest( DocumentRouteHeaderValue document, List<ActionRequestValue> requests, RouteNodeInstance nodeInstance, RouteContext routeContext ) throws RouteManagerException {
109                    boolean pastFinalApprover = isPastFinalApprover( document, nodeInstance );
110                    boolean hasApproveRequest = false;
111                    for ( ActionRequestValue actionRequest : requests ) {
112                            if ( actionRequest.isApproveOrCompleteRequest() ) {
113                                    hasApproveRequest = true;
114                                    break;
115                            }
116                    }
117                    // if final approver route level and no approve request send to
118                    // exception routing
119                    if ( nodeInstance.getRouteNode().getFinalApprovalInd().booleanValue() ) {
120                            // we must have an approve request generated if final
121                            // approver level.
122                            if ( !hasApproveRequest ) {
123                                    throw new RouteManagerException(
124                                                    "No Approve Request generated after final approver", routeContext );
125                            }
126                    } else if ( pastFinalApprover ) {
127                            // we can't allow generation of approve requests after final
128                            // approver. This guys going to exception routing.
129                            if ( hasApproveRequest ) {
130                                    throw new RouteManagerException(
131                                                    "Approve Request generated after final approver", routeContext );
132                            }
133                    }
134            }
135    
136    
137            /**
138             * @param routeLevel
139             *            Route level for which the action requests will be generated
140             * @param routeHeader
141             *            route header for which the action requests are generated
142             * @param saveFlag
143             *            if true the new action requests will be saved, if false they
144             *            are not written to the db
145             * @return List of ActionRequests - NOTE they are only written to DB if
146             *         saveFlag is set
147             * @throws WorkflowException
148             * @throws ResourceUnavailableException
149             */
150            public List<ActionRequestValue> getNewActionRequests(RouteContext context) throws Exception {
151                    RouteNodeInstance nodeInstance = context.getNodeInstance();
152                    String routeMethodName = nodeInstance.getRouteNode().getRouteMethodName();
153                    if ( LOG.isDebugEnabled() ) {
154                            LOG.debug( "Looking for action requests in " + routeMethodName + " : "
155                                            + nodeInstance.getRouteNode().getRouteNodeName() );
156                    }
157                    List<ActionRequestValue> newRequests = new ArrayList<ActionRequestValue>();
158                    try {
159                            RouteModule routeModule = getRouteModule( context );
160                            List<ActionRequestValue> requests = routeModule.findActionRequests( context );
161                            for ( ActionRequestValue actionRequest : requests ) {
162                                    if ( LOG.isDebugEnabled() ) {
163                                            LOG.debug( "Request generated by RouteModule '" + routeModule + "' for node "
164                                                            + nodeInstance + ":" + actionRequest );
165                                    }
166                                    actionRequest = KEWServiceLocator.getActionRequestService()
167                                                    .initializeActionRequestGraph( actionRequest, context.getDocument(),
168                                                                    nodeInstance );
169                                    saveActionRequest( context, actionRequest );
170                                    newRequests.add( actionRequest );
171                            }
172                    } catch ( WorkflowException ex ) {
173                            LOG.warn( "Caught WorkflowException during routing", ex );
174                            throw new RouteManagerException( ex, context );
175                    }
176                    return newRequests;
177            }
178    
179            /**
180             * Returns the RouteModule which should handle generating requests for this
181             * RequestsNode.
182             */
183            protected RouteModule getRouteModule(RouteContext context) throws Exception {
184                    return KEWServiceLocator.getRouteModuleService().findRouteModule(
185                                    context.getNodeInstance().getRouteNode() );
186            }
187    
188            /**
189             * Checks if the document has past the final approver node by walking
190             * backward through the previous node instances. Ignores any previous nodes
191             * that have been "revoked".
192             */
193            protected boolean isPastFinalApprover(DocumentRouteHeaderValue document,
194                            RouteNodeInstance nodeInstance) {
195                    FinalApproverContext context = new FinalApproverContext();
196                    List revokedNodeInstances = KEWServiceLocator.getRouteNodeService()
197                                    .getRevokedNodeInstances( document );
198                    Set revokedNodeInstanceIds = new HashSet();
199                    for ( Iterator iterator = revokedNodeInstances.iterator(); iterator.hasNext(); ) {
200                            RouteNodeInstance revokedNodeInstance = (RouteNodeInstance)iterator.next();
201                            revokedNodeInstanceIds.add( revokedNodeInstance.getRouteNodeInstanceId() );
202                    }
203                    isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
204                                    revokedNodeInstanceIds );
205                    return context.isPast;
206            }
207    
208            protected void isPastFinalApprover(List previousNodeInstances, FinalApproverContext context,
209                            Set revokedNodeInstanceIds) {
210                    if ( previousNodeInstances != null && !previousNodeInstances.isEmpty() ) {
211                            for ( Iterator iterator = previousNodeInstances.iterator(); iterator.hasNext(); ) {
212                                    if ( context.isPast ) {
213                                            return;
214                                    }
215                                    RouteNodeInstance nodeInstance = (RouteNodeInstance)iterator.next();
216                                    if ( context.inspected.contains( getKey( nodeInstance ) ) ) {
217                                            continue;
218                                    } else {
219                                            context.inspected.add( getKey( nodeInstance ) );
220                                    }
221                                    if ( Boolean.TRUE.equals( nodeInstance.getRouteNode().getFinalApprovalInd() ) ) {
222                                            // if the node instance has been revoked (by a Return To
223                                            // Previous action for example)
224                                            // then we don't want to consider that node when we
225                                            // determine if we are past final
226                                            // approval or not
227                                            if ( !revokedNodeInstanceIds.contains( nodeInstance.getRouteNodeInstanceId() ) ) {
228                                                    context.isPast = true;
229                                            }
230                                            return;
231                                    }
232                                    isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
233                                                    revokedNodeInstanceIds );
234                            }
235                    }
236            }
237    
238            /**
239             * The method will get a key value which can be used for comparison
240             * purposes. If the node instance has a primary key value, it will be
241             * returned. However, if the node instance has not been saved to the
242             * database (i.e. during a simulation) this method will return the node
243             * instance passed in.
244             */
245            protected Object getKey(RouteNodeInstance nodeInstance) {
246                    Long id = nodeInstance.getRouteNodeInstanceId();
247                    return (id != null ? (Object)id : (Object)nodeInstance);
248            }
249    
250            protected class FinalApproverContext {
251                    public Set inspected = new HashSet();
252    
253                    public boolean isPast = false;
254            }
255    
256            public static boolean isSupressingPolicyErrors(RouteContext routeContext) {
257                    Boolean suppressPolicyErrors = (Boolean)routeContext.getParameters().get(
258                                    SUPPRESS_POLICY_ERRORS_KEY );
259                    if ( suppressPolicyErrors == null || !suppressPolicyErrors ) {
260                            return false;
261                    }
262                    return true;
263            }
264    
265            @SuppressWarnings("unchecked")
266            public static void setSupressPolicyErrors(RouteContext routeContext) {
267                    routeContext.getParameters().put( SUPPRESS_POLICY_ERRORS_KEY, Boolean.TRUE );
268            }
269    }