001/**
002 * Copyright 2005-2016 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.Map;
023import java.util.Set;
024
025import org.apache.commons.lang.StringUtils;
026import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
027import org.kuali.rice.kew.actionrequest.ActionRequestValue;
028import org.kuali.rice.kew.api.exception.WorkflowException;
029import org.kuali.rice.kew.api.rule.Rule;
030import org.kuali.rice.kew.engine.RouteContext;
031import org.kuali.rice.kew.engine.RouteHelper;
032import org.kuali.rice.kew.exception.RouteManagerException;
033import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
034import org.kuali.rice.kew.rule.FlexRM;
035import org.kuali.rice.kew.rule.KRAMetaRuleEngine;
036import org.kuali.rice.kew.rule.RuleExpressionResult;
037import org.kuali.rice.kew.service.KEWServiceLocator;
038import 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 */
046public 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 = isSuppressingPolicyErrors(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 isSuppressingPolicyErrors(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}