001 /** 002 * Copyright 2005-2013 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.role; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.apache.commons.lang.exception.ExceptionUtils; 020 import org.kuali.rice.core.api.exception.RiceRuntimeException; 021 import org.kuali.rice.core.api.reflect.ObjectDefinition; 022 import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader; 023 import org.kuali.rice.kew.actionrequest.ActionRequestFactory; 024 import org.kuali.rice.kew.actionrequest.ActionRequestValue; 025 import org.kuali.rice.kew.api.KewApiServiceLocator; 026 import org.kuali.rice.kew.api.action.ActionRequestPolicy; 027 import org.kuali.rice.kew.api.exception.WorkflowException; 028 import org.kuali.rice.kew.api.extension.ExtensionDefinition; 029 import org.kuali.rice.kew.api.extension.ExtensionUtils; 030 import org.kuali.rice.kew.engine.RouteContext; 031 import org.kuali.rice.kew.engine.node.RouteNodeUtils; 032 import org.kuali.rice.kew.routemodule.RouteModule; 033 import org.kuali.rice.kew.rule.XmlConfiguredAttribute; 034 import org.kuali.rice.kew.rule.bo.RuleAttribute; 035 import org.kuali.rice.kew.service.KEWServiceLocator; 036 import org.kuali.rice.kew.api.KewApiConstants; 037 import org.kuali.rice.kew.util.ResponsibleParty; 038 import org.kuali.rice.kim.api.KimConstants; 039 import org.kuali.rice.kim.api.responsibility.ResponsibilityAction; 040 import org.kuali.rice.kim.api.responsibility.ResponsibilityService; 041 import org.kuali.rice.kim.api.services.KimApiServiceLocator; 042 043 import java.util.ArrayList; 044 import java.util.Collections; 045 import java.util.HashMap; 046 import java.util.List; 047 import java.util.Map; 048 049 /** 050 * The RoleRouteModule is responsible for interfacing with the KIM 051 * Role system to provide Role-based routing to KEW. 052 * 053 * @author Kuali Rice Team (rice.collab@kuali.org) 054 */ 055 public class RoleRouteModule implements RouteModule { 056 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(RoleRouteModule.class); 057 058 protected static final String QUALIFIER_RESOLVER_ELEMENT = KewApiConstants.ROLEROUTE_QUALIFIER_RESOLVER_ELEMENT; 059 protected static final String QUALIFIER_RESOLVER_CLASS_ELEMENT = KewApiConstants.ROLEROUTE_QUALIFIER_RESOLVER_CLASS_ELEMENT; 060 protected static final String RESPONSIBILITY_TEMPLATE_NAME_ELEMENT = KewApiConstants.ROLEROUTE_RESPONSIBILITY_TEMPLATE_NAME_ELEMENT; 061 protected static final String NAMESPACE_ELEMENT = KewApiConstants.ROLEROUTE_NAMESPACE_ELEMENT; 062 063 private static ResponsibilityService responsibilityService; 064 065 private String qualifierResolverName; 066 private String qualifierResolverClassName; 067 private String responsibilityTemplateName; 068 private String namespace; 069 070 @Override 071 public boolean isMoreRequestsAvailable(RouteContext context) { 072 return false; 073 } 074 075 @SuppressWarnings("unchecked") 076 public List<ActionRequestValue> findActionRequests(RouteContext context) 077 throws Exception { 078 079 ActionRequestFactory arFactory = new ActionRequestFactory(context.getDocument(), context.getNodeInstance()); 080 081 QualifierResolver qualifierResolver = loadQualifierResolver(context); 082 List<Map<String, String>> qualifiers = qualifierResolver.resolve(context); 083 String responsibilityTemplateName = loadResponsibilityTemplateName(context); 084 String namespaceCode = loadNamespace(context); 085 Map<String, String> responsibilityDetails = loadResponsibilityDetails(context); 086 if (LOG.isDebugEnabled()) { 087 logQualifierCheck(namespaceCode, responsibilityTemplateName, responsibilityDetails, qualifiers); 088 } 089 if ( qualifiers != null ) { 090 for (Map<String, String> qualifier : qualifiers) { 091 if ( qualifier.containsKey( KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER ) ) { 092 responsibilityDetails.put(KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER, qualifier.get(KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER)); 093 } else { 094 responsibilityDetails.remove( KimConstants.AttributeConstants.QUALIFIER_RESOLVER_PROVIDED_IDENTIFIER ); 095 } 096 List<ResponsibilityAction> responsibilities = getResponsibilityService().getResponsibilityActionsByTemplate( 097 namespaceCode, responsibilityTemplateName, qualifier, responsibilityDetails); 098 if (LOG.isDebugEnabled()) { 099 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 }