View Javadoc

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