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.Map;
023    import java.util.Set;
024    
025    import org.apache.commons.lang.StringUtils;
026    import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
027    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
028    import org.kuali.rice.kew.api.exception.WorkflowException;
029    import org.kuali.rice.kew.api.rule.Rule;
030    import org.kuali.rice.kew.engine.RouteContext;
031    import org.kuali.rice.kew.engine.RouteHelper;
032    import org.kuali.rice.kew.exception.RouteManagerException;
033    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
034    import org.kuali.rice.kew.rule.FlexRM;
035    import org.kuali.rice.kew.rule.KRAMetaRuleEngine;
036    import org.kuali.rice.kew.rule.RuleExpressionResult;
037    import org.kuali.rice.kew.service.KEWServiceLocator;
038    import org.kuali.rice.kew.util.Utilities;
039    
040    
041    /**
042     * Node that implements a KRAMetaRule, with multiple request/response phases
043     *
044     * @author Kuali Rice Team (rice.collab@kuali.org)
045     */
046    public class KRAMetaRuleNode extends IteratedRequestActivationNode {
047            private static String SUPPRESS_POLICY_ERRORS_KEY = "_suppressPolicyErrorsRequestActivationNode";
048    
049            protected List<ActionRequestValue> generateUninitializedRequests(ActionRequestFactory arFactory, RouteContext context, RuleExpressionResult result) throws WorkflowException {
050                    FlexRM flexRM = new FlexRM();
051                    flexRM.makeActionRequests(arFactory, result.getResponsibilities(), context, Rule.Builder.create(result.getRule().getDefinition()).build(), context.getDocument(), null, null);
052                    return new ArrayList<ActionRequestValue>(arFactory.getRequestGraphs());
053            }
054    
055            protected List<ActionRequestValue> initializeRequests(List<ActionRequestValue> requests, RouteContext context) {
056                    // RequestNode.getNewActionRequests
057                    List<ActionRequestValue> newRequests = new ArrayList<ActionRequestValue>();
058                    for (Iterator iterator = requests.iterator(); iterator.hasNext();) {
059                            ActionRequestValue actionRequest = (ActionRequestValue) iterator.next();
060                            actionRequest = KEWServiceLocator.getActionRequestService().initializeActionRequestGraph(actionRequest, context.getDocument(), context.getNodeInstance());
061                            saveActionRequest(context, actionRequest);
062                            newRequests.add(actionRequest);
063                    }
064                    return newRequests;
065            }
066    
067            @Override
068            protected boolean generateNewRequests(boolean initial, RouteContext context, RouteHelper routeHelper)
069            throws WorkflowException, Exception {
070                    RouteNodeInstance nodeInstance = context.getNodeInstance();
071                    RouteNode nodeDef = nodeInstance.getRouteNode();
072    
073                    // get the expression
074                    Map<String, String> cfg = Utilities.getKeyValueCollectionAsMap(nodeDef.getConfigParams());
075                    String expression = cfg.get("expression");
076                    if (StringUtils.isEmpty(expression)) {
077                            throw new WorkflowException("No meta-rule expression supplied in node " + nodeDef);
078                    }
079                    KRAMetaRuleEngine engine = new KRAMetaRuleEngine(expression);
080    
081                    // get the current statement
082                    int curStatement = getCurStatement(nodeInstance);
083    
084                    engine.setCurStatement(curStatement);
085    
086                    if (engine.isDone()) {
087                            return false;
088                    }
089    
090                    // generate next round of action requests
091                    RuleExpressionResult result = engine.processSingleStatement(context);
092                    if (!result.isSuccess()) {
093                            return false;
094                    }
095    
096                    boolean suppressPolicyErrors = isSupressingPolicyErrors(context);
097                    boolean pastFinalApprover = isPastFinalApprover(context.getDocument(), nodeInstance);
098    
099                    // actionRequests.addAll(makeActionRequests(context, rule, routeHeader, null, null));
100                    // copied from parts of FlexRM and RequestsNode
101                    ActionRequestFactory arFactory = new ActionRequestFactory(context.getDocument(), context.getNodeInstance());
102                    List<ActionRequestValue> requests = generateUninitializedRequests(arFactory, context, result);
103                    requests = initializeRequests(requests, context);
104                    // RequestNode
105                    if ((requests.isEmpty()) && nodeDef.getMandatoryRouteInd().booleanValue() && ! suppressPolicyErrors) {
106                            LOG.warn("no requests generated for mandatory route - " + nodeDef.getRouteNodeName());
107                            throw new RouteManagerException("No requests generated for mandatory route " + nodeDef.getRouteNodeName() + ":" + nodeDef.getRouteMethodName(), context);
108                    }
109                    // determine if we have any approve requests for FinalApprover checks
110                    boolean hasApproveRequest = false;
111                    for (Iterator iter = requests.iterator(); iter.hasNext();) {
112                            ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
113                            hasApproveRequest = actionRequest.isApproveOrCompleteRequest() || hasApproveRequest;
114                    }
115                    // if final approver route level and no approve request send to exception routing
116                    if (nodeDef.getFinalApprovalInd().booleanValue()) {
117                            // we must have an approve request generated if final approver level.
118                            if (!hasApproveRequest && ! suppressPolicyErrors) {
119                                    throw new RuntimeException("No Approve Request generated after final approver");
120                            }
121                    } else if (pastFinalApprover) {
122                            // we can't allow generation of approve requests after final approver. This guys going to exception routing.
123                            if (hasApproveRequest && ! suppressPolicyErrors) {
124                                    throw new RuntimeException("Approve Request generated after final approver");
125                            }
126                    }
127    
128                    // increment to next statement
129                    nodeInstance.getNodeState("stmt").setValue(String.valueOf(curStatement + 1));
130                    return !requests.isEmpty();
131            }
132    
133            /**
134             * @param nodeInstance the current node instance under execution
135             * @return the meta-rule statement this node instance should process
136             */
137            protected static int getCurStatement(RouteNodeInstance nodeInstance) {
138                    int statement = 0;
139                    NodeState nodeState = nodeInstance.getNodeState("stmt");
140                    if (nodeState == null) {
141                            nodeState = new NodeState();
142                            nodeState.setKey("stmt");
143                            nodeState.setNodeInstance(nodeInstance);
144                            nodeInstance.addNodeState(nodeState);
145                    }
146                    if (StringUtils.isEmpty(nodeState.getValue())) {
147                            nodeState.setValue("0");
148                    } else {
149                            statement = Integer.parseInt(nodeState.getValue());
150                    }
151                    return statement;
152            }
153    
154            @Override
155            protected RequestFulfillmentCriteria getRequestFulfillmentCriteria(RouteContext routeContext) {
156                    return super.getRequestFulfillmentCriteria(routeContext);
157            }
158    
159    
160            // -- copied from request node; a lot of this action request evaluating code should probably go into helper classes or factored into a common subclass
161    
162            /**
163             * The method will get a key value which can be used for comparison purposes. If the node instance has a primary key value, it will be returned. However, if the node instance has not been saved to the database (i.e. during a simulation) this method will return the node instance passed in.
164             */
165            private Object getKey(RouteNodeInstance nodeInstance) {
166                    String id = nodeInstance.getRouteNodeInstanceId();
167                    return (id != null ? (Object) id : (Object) nodeInstance);
168            }
169    
170            /**
171             * Checks if the document has past the final approver node by walking backward through the previous node instances.
172             * Ignores any previous nodes that have been "revoked".
173             */
174            private boolean isPastFinalApprover(DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance) {
175                    FinalApproverContext context = new FinalApproverContext();
176                    List revokedNodeInstances = KEWServiceLocator.getRouteNodeService().getRevokedNodeInstances(document);
177                    Set revokedNodeInstanceIds = new HashSet();
178                    for (Iterator iterator = revokedNodeInstances.iterator(); iterator.hasNext(); ) {
179                            RouteNodeInstance revokedNodeInstance = (RouteNodeInstance) iterator.next();
180                            revokedNodeInstanceIds.add(revokedNodeInstance.getRouteNodeInstanceId());
181                    }
182                    isPastFinalApprover(nodeInstance.getPreviousNodeInstances(), context, revokedNodeInstanceIds);
183                    return context.isPast;
184            }
185    
186            private void isPastFinalApprover(List previousNodeInstances, FinalApproverContext context, Set revokedNodeInstanceIds) {
187                    if (previousNodeInstances != null && !previousNodeInstances.isEmpty()) {
188                            for (Iterator iterator = previousNodeInstances.iterator(); iterator.hasNext();) {
189                                    if (context.isPast) {
190                                            return;
191                                    }
192                                    RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next();
193                                    if (context.inspected.contains(getKey(nodeInstance))) {
194                                            continue;
195                                    } else {
196                                            context.inspected.add(getKey(nodeInstance));
197                                    }
198                                    if (Boolean.TRUE.equals(nodeInstance.getRouteNode().getFinalApprovalInd())) {
199                                            // if the node instance has been revoked (by a Return To Previous action for example)
200                                            // then we don't want to consider that node when we determine if we are past final
201                                            // approval or not
202                                            if (!revokedNodeInstanceIds.contains(nodeInstance.getRouteNodeInstanceId())) {
203                                                    context.isPast = true;
204                                            }
205                                            return;
206                                    }
207                                    isPastFinalApprover(nodeInstance.getPreviousNodeInstances(), context, revokedNodeInstanceIds);
208                            }
209                    }
210            }
211            public static boolean isSupressingPolicyErrors(RouteContext routeContext) {
212                    Boolean suppressPolicyErrors = (Boolean)routeContext.getParameters().get(SUPPRESS_POLICY_ERRORS_KEY);
213                    if (suppressPolicyErrors == null || ! suppressPolicyErrors) {
214                            return false;
215                    }
216                    return true;
217            }
218    
219            private class FinalApproverContext {
220                    public Set inspected = new HashSet();
221                    public boolean isPast = false;
222            }
223    
224    }