View Javadoc

1   /**
2    * Copyright 2005-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.role;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.commons.lang.exception.ExceptionUtils;
20  import org.kuali.rice.core.api.exception.RiceRuntimeException;
21  import org.kuali.rice.core.api.reflect.ObjectDefinition;
22  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
23  import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
24  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
25  import org.kuali.rice.kew.api.action.ActionRequestPolicy;
26  import org.kuali.rice.kew.api.exception.WorkflowException;
27  import org.kuali.rice.kew.engine.RouteContext;
28  import org.kuali.rice.kew.engine.node.RouteNodeUtils;
29  import org.kuali.rice.kew.routemodule.RouteModule;
30  import org.kuali.rice.kew.rule.XmlConfiguredAttribute;
31  import org.kuali.rice.kew.rule.bo.RuleAttribute;
32  import org.kuali.rice.kew.service.KEWServiceLocator;
33  import org.kuali.rice.kew.api.KewApiConstants;
34  import org.kuali.rice.kew.util.ResponsibleParty;
35  import org.kuali.rice.kim.api.KimConstants;
36  import org.kuali.rice.kim.api.responsibility.ResponsibilityAction;
37  import org.kuali.rice.kim.api.responsibility.ResponsibilityService;
38  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
39  
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.HashMap;
43  import java.util.List;
44  import java.util.Map;
45  
46  /**
47   * The RoleRouteModule is responsible for interfacing with the KIM
48   * Role system to provide Role-based routing to KEW. 
49   * 
50   * @author Kuali Rice Team (rice.collab@kuali.org)
51   */
52  public class RoleRouteModule implements RouteModule {
53      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(RoleRouteModule.class);
54  	
55  	protected static final String QUALIFIER_RESOLVER_ELEMENT = KewApiConstants.ROLEROUTE_QUALIFIER_RESOLVER_ELEMENT;
56  	protected static final String QUALIFIER_RESOLVER_CLASS_ELEMENT = KewApiConstants.ROLEROUTE_QUALIFIER_RESOLVER_CLASS_ELEMENT;
57  	protected static final String RESPONSIBILITY_TEMPLATE_NAME_ELEMENT = KewApiConstants.ROLEROUTE_RESPONSIBILITY_TEMPLATE_NAME_ELEMENT;
58  	protected static final String NAMESPACE_ELEMENT = KewApiConstants.ROLEROUTE_NAMESPACE_ELEMENT;
59  	
60  	private static ResponsibilityService responsibilityService;
61  	
62  	private String qualifierResolverName;
63  	private String qualifierResolverClassName;
64  	private String responsibilityTemplateName;
65  	private String namespace;
66  
67      @Override
68      public boolean isMoreRequestsAvailable(RouteContext context) {
69          return false;
70      }
71  
72  	@SuppressWarnings("unchecked")
73  	public List<ActionRequestValue> findActionRequests(RouteContext context)
74  			throws Exception {
75  		
76  		ActionRequestFactory arFactory = new ActionRequestFactory(context.getDocument(), context.getNodeInstance());
77  
78  		QualifierResolver qualifierResolver = loadQualifierResolver(context);
79  		List<Map<String, String>> qualifiers = qualifierResolver.resolve(context);
80  		String responsibilityTemplateName = loadResponsibilityTemplateName(context);
81  		String namespaceCode = loadNamespace(context);
82  		Map<String, String> responsibilityDetails = loadResponsibilityDetails(context);
83  		if (LOG.isDebugEnabled()) {
84  			logQualifierCheck(namespaceCode, responsibilityTemplateName, responsibilityDetails, qualifiers);
85  		}
86  		if ( qualifiers != null ) {
87  			for (Map<String, String> qualifier : qualifiers) {
88  				if ( qualifier.containsKey( KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER ) ) {
89  					responsibilityDetails.put(KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER, qualifier.get(KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER));
90  				} else {
91  					responsibilityDetails.remove( KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER );
92  				}
93  				List<ResponsibilityAction> responsibilities = getResponsibilityService().getResponsibilityActionsByTemplateName(namespaceCode, responsibilityTemplateName,
94                          qualifier, responsibilityDetails);
95  				if (LOG.isDebugEnabled()) {
96  					LOG.debug("Found " + responsibilities.size() + " responsibilities from ResponsibilityService");
97  				}
98  				// split the responsibility list defining characteristics (per the ResponsibilitySet.matches() method)
99  				List<ResponsibilitySet> responsibilitySets = partitionResponsibilities(responsibilities);
100 				if (LOG.isDebugEnabled()) {
101 					LOG.debug("Found " + responsibilitySets.size() + " responsibility sets from ResponsibilityActionInfo list");
102 				}
103 				for (ResponsibilitySet responsibilitySet : responsibilitySets) {
104 					String approvePolicy = responsibilitySet.getApprovePolicy();
105 					// if all must approve, add the responsibilities individually so that the each get their own approval graph
106 					if (ActionRequestPolicy.ALL.getCode().equals(approvePolicy)) {
107 						for (ResponsibilityAction responsibility : responsibilitySet.getResponsibilities()) {
108 							arFactory.addRoleResponsibilityRequest(Collections.singletonList(responsibility), approvePolicy);
109 						}
110 					} else {
111 						// first-approve policy, pass as groups to the ActionRequestFactory so that a single approval per set will clear the action request
112 						arFactory.addRoleResponsibilityRequest(responsibilitySet.getResponsibilities(), approvePolicy);
113 					}
114 				}
115 			}		
116 		}
117 		List<ActionRequestValue> actionRequests = new ArrayList<ActionRequestValue>(arFactory.getRequestGraphs());
118 		disableResolveResponsibility(actionRequests);
119 		return actionRequests;
120 	}
121 	
122     protected void logQualifierCheck(String namespaceCode, String responsibilityName, Map<String, String> responsibilityDetails, List<Map<String, String>> qualifiers ) {
123 		StringBuilder sb = new StringBuilder();
124 		sb.append(  '\n' );
125 		sb.append( "Get Resp Actions: " ).append( namespaceCode ).append( "/" ).append( responsibilityName ).append( '\n' );
126 		sb.append( "             Details:\n" );
127 		if ( responsibilityDetails != null ) {
128 			sb.append( responsibilityDetails );
129 		} else {
130 			sb.append( "                         [null]\n" );
131 		}
132 		sb.append( "             Qualifiers:\n" );
133 		for (Map<String, String> qualification : qualifiers) {
134 			if ( qualification != null ) {
135 				sb.append( qualification );
136 			} else {
137 				sb.append( "                         [null]\n" );
138 			}
139 		}
140 		if (LOG.isTraceEnabled()) { 
141 			LOG.trace( sb.append(ExceptionUtils.getStackTrace(new Throwable())));
142 		} else {
143 			LOG.debug(sb.toString());
144 		}
145     }
146 
147     /**
148 	 * Walks the ActionRequest graph and disables responsibility resolution on those ActionRequests.
149 	 * Because of the fact that it's not possible to tell if an ActionRequest was generated by
150 	 * KIM once it's been saved in the database, we want to disable responsibilityId
151 	 * resolution on the RouteModule because we will end up geting a reference to FlexRM and
152 	 * a call to resolveResponsibilityId will fail.
153 	 * 
154 	 * @param actionRequests
155 	 */
156 	protected void disableResolveResponsibility(List<ActionRequestValue> actionRequests) {
157 		for (ActionRequestValue actionRequest : actionRequests) {
158 			actionRequest.setResolveResponsibility(false);
159 			disableResolveResponsibility(actionRequest.getChildrenRequests());
160 		}
161 	}
162 
163 	protected QualifierResolver loadQualifierResolver(RouteContext context) {
164 		if (StringUtils.isBlank(qualifierResolverName)) {
165 			this.qualifierResolverName = RouteNodeUtils.getValueOfCustomProperty(context.getNodeInstance().getRouteNode(), QUALIFIER_RESOLVER_ELEMENT);
166 		}
167 		if (StringUtils.isBlank(qualifierResolverClassName)) {			
168 			this.qualifierResolverClassName = RouteNodeUtils.getValueOfCustomProperty(context.getNodeInstance().getRouteNode(), QUALIFIER_RESOLVER_CLASS_ELEMENT);
169 		}
170 		QualifierResolver resolver = null;
171 		if (!StringUtils.isBlank(qualifierResolverName)) {
172 			RuleAttribute ruleAttribute = KEWServiceLocator.getRuleAttributeService().findByName(qualifierResolverName);
173 			if (ruleAttribute == null) {
174 				throw new RiceRuntimeException("Failed to locate QualifierResolver for attribute name: " + qualifierResolverName);
175 			}
176 			ObjectDefinition definition = getAttributeObjectDefinition(ruleAttribute);
177 			resolver = (QualifierResolver)GlobalResourceLoader.getObject(definition);
178 			if (resolver instanceof XmlConfiguredAttribute) {
179 				((XmlConfiguredAttribute)resolver).setExtensionDefinition(RuleAttribute.to(ruleAttribute));
180 			}
181 		}
182 		if (resolver == null && !StringUtils.isBlank(qualifierResolverClassName)) {
183 			resolver = (QualifierResolver)GlobalResourceLoader.getObject(new ObjectDefinition(qualifierResolverClassName));
184 		}
185 		if (resolver == null) {
186 			resolver = new NullQualifierResolver();
187 		}
188 		if (LOG.isDebugEnabled()) {
189 			LOG.debug("Resolver class being returned: " + resolver.getClass().getName());
190 		}
191 		return resolver;
192 	}
193 	
194 	protected Map<String, String> loadResponsibilityDetails(RouteContext context) {
195 		String documentTypeName = context.getDocument().getDocumentType().getName();
196 		String nodeName = context.getNodeInstance().getName();
197 		Map<String, String> responsibilityDetails = new HashMap<String, String>();
198 		responsibilityDetails.put(KewApiConstants.DOCUMENT_TYPE_NAME_DETAIL, documentTypeName);
199 		responsibilityDetails.put(KewApiConstants.ROUTE_NODE_NAME_DETAIL, nodeName);
200 		return responsibilityDetails;
201 	}
202 	
203 	protected String loadResponsibilityTemplateName(RouteContext context) {
204 		if (StringUtils.isBlank(responsibilityTemplateName)) {
205 			this.responsibilityTemplateName = RouteNodeUtils.getValueOfCustomProperty(context.getNodeInstance().getRouteNode(), RESPONSIBILITY_TEMPLATE_NAME_ELEMENT);
206 		}
207 		if (StringUtils.isBlank(responsibilityTemplateName)) {
208 			this.responsibilityTemplateName = KewApiConstants.DEFAULT_RESPONSIBILITY_TEMPLATE_NAME;
209 		}
210 		return responsibilityTemplateName;
211 	}
212 	
213 	protected String loadNamespace(RouteContext context) {
214 		if (StringUtils.isBlank(namespace)) {
215 			this.namespace = RouteNodeUtils.getValueOfCustomProperty(context.getNodeInstance().getRouteNode(), NAMESPACE_ELEMENT);
216 		}
217 		if (StringUtils.isBlank(namespace)) {
218 			this.namespace = KewApiConstants.KEW_NAMESPACE;
219 		}
220 		return namespace;
221 	}
222 	
223     protected ObjectDefinition getAttributeObjectDefinition(RuleAttribute ruleAttribute) {
224     	return new ObjectDefinition(ruleAttribute.getResourceDescriptor(), ruleAttribute.getApplicationId());
225     }
226     
227     protected List<ResponsibilitySet> partitionResponsibilities(List<ResponsibilityAction> responsibilities) {
228     	List<ResponsibilitySet> responsibilitySets = new ArrayList<ResponsibilitySet>();
229     	for (ResponsibilityAction responsibility : responsibilities) {
230     		ResponsibilitySet targetResponsibilitySet = null;
231     		for (ResponsibilitySet responsibiliySet : responsibilitySets) {
232     			if (responsibiliySet.matches(responsibility)) {
233     				targetResponsibilitySet = responsibiliySet;
234     			}
235     		}
236     		if (targetResponsibilitySet == null) {
237     			targetResponsibilitySet = new ResponsibilitySet(responsibility);
238     			responsibilitySets.add(targetResponsibilitySet);
239     		}
240     		targetResponsibilitySet.getResponsibilities().add(responsibility);
241     	}
242     	return responsibilitySets;
243     }
244 	
245 	/**
246 	 * Return null so that the responsibility ID will remain the same.
247 	 *
248 	 * @see org.kuali.rice.kew.routemodule.RouteModule#resolveResponsibilityId(String)
249 	 */
250 	public ResponsibleParty resolveResponsibilityId(String responsibilityId)
251 			throws WorkflowException {
252 		return null;
253 	}
254 	
255 	
256 	
257 	private static class ResponsibilitySet {
258 		private String actionRequestCode;
259 		private String approvePolicy;
260 		private Integer priorityNumber;
261 		private String parallelRoutingGroupingCode;
262 		private String roleResponsibilityActionId;
263 		private List<ResponsibilityAction> responsibilities = new ArrayList<ResponsibilityAction>();
264 
265 		public ResponsibilitySet(ResponsibilityAction responsibility) {
266 			this.actionRequestCode = responsibility.getActionTypeCode();
267 			this.approvePolicy = responsibility.getActionPolicyCode();
268 			this.priorityNumber = responsibility.getPriorityNumber();
269 			this.parallelRoutingGroupingCode = responsibility.getParallelRoutingGroupingCode();
270 			this.roleResponsibilityActionId = responsibility.getRoleResponsibilityActionId();
271 		}
272 		
273 		public boolean matches(ResponsibilityAction responsibility) {
274 			return responsibility.getActionTypeCode().equals(actionRequestCode) &&
275 				responsibility.getActionPolicyCode().equals(approvePolicy) && 
276 				responsibility.getPriorityNumber().equals( priorityNumber ) &&
277 				responsibility.getParallelRoutingGroupingCode().equals( parallelRoutingGroupingCode ) &&
278 				responsibility.getRoleResponsibilityActionId().equals( roleResponsibilityActionId );
279 		}
280 
281 		public String getActionRequestCode() {
282 			return this.actionRequestCode;
283 		}
284 
285 		public String getApprovePolicy() {
286 			return this.approvePolicy;
287 		}
288 		
289 		public Integer getPriorityNumber() {
290 			return priorityNumber;
291 		}
292 
293 		public List<ResponsibilityAction> getResponsibilities() {
294 			return this.responsibilities;
295 		}
296 
297 		public String getParallelRoutingGroupingCode() {
298 			return this.parallelRoutingGroupingCode;
299 		}
300 
301 		public String getRoleResponsibilityActionId() {
302 			return this.roleResponsibilityActionId;
303 		}		
304 		
305 	}
306 
307 
308 
309 	/**
310 	 * @param qualifierResolverName the qualifierResolverName to set
311 	 */
312 	public void setQualifierResolverName(String qualifierResolverName) {
313 		this.qualifierResolverName = qualifierResolverName;
314 	}
315 
316 	/**
317 	 * @param qualifierResolverClassName the qualifierResolverClassName to set
318 	 */
319 	public void setQualifierResolverClassName(String qualifierResolverClassName) {
320 		this.qualifierResolverClassName = qualifierResolverClassName;
321 	}
322 
323 	/**
324 	 * @param responsibilityTemplateName the responsibilityTemplateName to set
325 	 */
326 	public void setResponsibilityTemplateName(String responsibilityTemplateName) {
327 		this.responsibilityTemplateName = responsibilityTemplateName;
328 	}
329 
330 	/**
331 	 * @param namespace the namespace to set
332 	 */
333 	public void setNamespace(String namespace) {
334 		this.namespace = namespace;
335 	}
336 
337 	protected ResponsibilityService getResponsibilityService() {
338 		if ( responsibilityService == null ) {
339 			responsibilityService = KimApiServiceLocator.getResponsibilityService();
340 		}
341 		return responsibilityService;
342 	}
343 
344 }