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 }