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.krad.workflow.service.impl; 017 018import java.text.MessageFormat; 019import java.util.ArrayList; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Set; 023 024import org.apache.commons.lang.StringUtils; 025import org.apache.commons.lang.time.StopWatch; 026import org.kuali.rice.core.api.CoreApiServiceLocator; 027import org.kuali.rice.core.api.exception.RiceRuntimeException; 028import org.kuali.rice.core.api.util.RiceKeyConstants; 029import org.kuali.rice.kew.api.KewApiConstants; 030import org.kuali.rice.kew.api.KewApiServiceLocator; 031import org.kuali.rice.kew.api.WorkflowDocument; 032import org.kuali.rice.kew.api.WorkflowDocumentFactory; 033import org.kuali.rice.kew.api.action.ActionRequestType; 034import org.kuali.rice.kew.api.action.ActionType; 035import org.kuali.rice.kew.api.document.node.RouteNodeInstance; 036import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 037import org.kuali.rice.kew.api.exception.WorkflowException; 038import org.kuali.rice.kim.api.group.Group; 039import org.kuali.rice.kim.api.identity.Person; 040import org.kuali.rice.kim.api.identity.principal.Principal; 041import org.kuali.rice.kim.api.services.KimApiServiceLocator; 042import org.kuali.rice.krad.bo.AdHocRoutePerson; 043import org.kuali.rice.krad.bo.AdHocRouteRecipient; 044import org.kuali.rice.krad.bo.AdHocRouteWorkgroup; 045import org.kuali.rice.krad.data.DataObjectService; 046import org.kuali.rice.krad.exception.UnknownDocumentIdException; 047import org.kuali.rice.krad.util.GlobalVariables; 048import org.kuali.rice.krad.workflow.service.WorkflowDocumentService; 049import org.springframework.transaction.annotation.Transactional; 050 051 052/** 053 * Implementation of the WorkflowDocumentService, which makes use of Workflow 054 * 055 * @author Kuali Rice Team (rice.collab@kuali.org) 056 */ 057@Transactional 058public class WorkflowDocumentServiceImpl implements WorkflowDocumentService { 059 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(WorkflowDocumentServiceImpl.class); 060 061 protected DataObjectService dataObjectService; 062 063 @Override 064 public boolean workflowDocumentExists(String documentId) { 065 boolean exists = false; 066 067 if (StringUtils.isBlank(documentId)) { 068 throw new IllegalArgumentException("invalid (blank) documentId"); 069 } 070 071 exists = KewApiServiceLocator.getWorkflowDocumentService().doesDocumentExist(documentId); 072 073 return exists; 074 } 075 076 @Override 077 public WorkflowDocument createWorkflowDocument(String documentTypeName, Person person) { 078 String watchName = "createWorkflowDocument"; 079 StopWatch watch = null; 080 if (LOG.isDebugEnabled()) { 081 watch = new StopWatch(); 082 watch.start(); 083 LOG.debug(watchName + ": started"); 084 } 085 if (StringUtils.isBlank(documentTypeName)) { 086 throw new IllegalArgumentException("invalid (blank) documentTypeName"); 087 } 088 if (person == null) { 089 throw new IllegalArgumentException("invalid (null) person"); 090 } 091 092 if (StringUtils.isBlank(person.getPrincipalName())) { 093 throw new IllegalArgumentException("invalid (empty) PrincipalName"); 094 } 095 096 if (LOG.isDebugEnabled()) { 097 LOG.debug("creating workflowDoc(" + documentTypeName + "," + person.getPrincipalName() + ")"); 098 } 099 100 WorkflowDocument document = WorkflowDocumentFactory.createDocument(person.getPrincipalId(), documentTypeName); 101 if ( watch != null ) { 102 watch.stop(); 103 LOG.debug(watchName + ": " + watch.toString()); 104 } 105 106 return document; 107 } 108 109 @Override 110 public WorkflowDocument loadWorkflowDocument(String documentId, Person user) { 111 if (documentId == null) { 112 throw new IllegalArgumentException("invalid (null) documentHeaderId"); 113 } 114 if (user == null) { 115 throw new IllegalArgumentException("invalid (null) workflowUser"); 116 } 117 else if (StringUtils.isEmpty(user.getPrincipalName())) { 118 throw new IllegalArgumentException("invalid (empty) workflowUser"); 119 } 120 121 if (LOG.isDebugEnabled()) { 122 LOG.debug("retrieving document(" + documentId + "," + user.getPrincipalName() + ")"); 123 } 124 125 try { 126 return WorkflowDocumentFactory.loadDocument(user.getPrincipalId(), documentId); 127 } catch (IllegalArgumentException e) { 128 // TODO do we really need to do this or just let the IllegalArgument propogate? 129 throw new UnknownDocumentIdException("unable to locate document with documentHeaderId '" + documentId + "'"); 130 } 131 } 132 133 @Override 134 public void acknowledge(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException { 135 if (LOG.isDebugEnabled()) { 136 LOG.debug("acknowleding document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 137 } 138 139 handleAdHocRouteRequests(workflowDocument, annotation, filterAdHocRecipients(adHocRecipients, new String[] { KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, KewApiConstants.ACTION_REQUEST_FYI_REQ })); 140 workflowDocument.acknowledge(annotation); 141 } 142 143 @Override 144 public void approve(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException { 145 if (LOG.isDebugEnabled()) { 146 LOG.debug("approving document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 147 } 148 149 handleAdHocRouteRequests(workflowDocument, annotation, filterAdHocRecipients(adHocRecipients, new String[] { KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, KewApiConstants.ACTION_REQUEST_FYI_REQ, KewApiConstants.ACTION_REQUEST_APPROVE_REQ })); 150 workflowDocument.approve(annotation); 151 } 152 153 154 @Override 155 public void superUserApprove(WorkflowDocument workflowDocument, String annotation) throws WorkflowException { 156 if ( LOG.isInfoEnabled() ) { 157 LOG.info("super user approve document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 158 } 159 workflowDocument.superUserBlanketApprove(annotation); 160 } 161 162 @Override 163 public void superUserCancel(WorkflowDocument workflowDocument, String annotation) throws WorkflowException { 164 if ( LOG.isInfoEnabled() ) { 165 LOG.info("super user cancel document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 166 } 167 workflowDocument.superUserCancel(annotation); 168 } 169 170 @Override 171 public void superUserDisapprove(WorkflowDocument workflowDocument, String annotation) throws WorkflowException { 172 if ( LOG.isInfoEnabled() ) { 173 LOG.info("super user disapprove document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 174 } 175 workflowDocument.superUserDisapprove(annotation); 176 } 177 178 @Override 179 public void blanketApprove(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException { 180 if (LOG.isDebugEnabled()) { 181 LOG.debug("blanket approving document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 182 } 183 184 handleAdHocRouteRequests(workflowDocument, annotation, filterAdHocRecipients(adHocRecipients, new String[] { KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, KewApiConstants.ACTION_REQUEST_FYI_REQ })); 185 workflowDocument.blanketApprove(annotation); 186 } 187 188 @Override 189 public void cancel(WorkflowDocument workflowDocument, String annotation) throws WorkflowException { 190 if (LOG.isDebugEnabled()) { 191 LOG.debug("canceling document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 192 } 193 194 workflowDocument.cancel(annotation); 195 } 196 197 @Override 198 public void recall(WorkflowDocument workflowDocument, String annotation, boolean cancel) throws WorkflowException { 199 if (LOG.isDebugEnabled()) { 200 LOG.debug("recalling document(" + workflowDocument.getDocumentId() + ",'" + annotation + "', '" + cancel + "')"); 201 } 202 203 workflowDocument.recall(annotation, cancel); 204 } 205 206 @Override 207 public void clearFyi(WorkflowDocument workflowDocument, List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException { 208 if (LOG.isDebugEnabled()) { 209 LOG.debug("clearing FYI for document(" + workflowDocument.getDocumentId() + ")"); 210 } 211 212 handleAdHocRouteRequests(workflowDocument, "", filterAdHocRecipients(adHocRecipients, new String[] { KewApiConstants.ACTION_REQUEST_FYI_REQ })); 213 workflowDocument.fyi(); 214 } 215 216 @Override 217 public void sendWorkflowNotification(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException { 218 sendWorkflowNotification(workflowDocument, annotation, adHocRecipients, null); 219 } 220 221 @Override 222 public void sendWorkflowNotification(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients, String notificationLabel) throws WorkflowException { 223 if (LOG.isDebugEnabled()) { 224 LOG.debug("sending FYI for document(" + workflowDocument.getDocumentId() + ")"); 225 } 226 227 handleAdHocRouteRequests(workflowDocument, annotation, adHocRecipients, notificationLabel); 228 } 229 230 @Override 231 public void disapprove(WorkflowDocument workflowDocument, String annotation) throws WorkflowException { 232 if (LOG.isDebugEnabled()) { 233 LOG.debug("disapproving document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 234 } 235 236 workflowDocument.disapprove(annotation); 237 } 238 239 @Override 240 public void route(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException { 241 if (LOG.isDebugEnabled()) { 242 LOG.debug("routing document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 243 } 244 245 handleAdHocRouteRequests(workflowDocument, annotation, filterAdHocRecipients(adHocRecipients, new String[] { KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, KewApiConstants.ACTION_REQUEST_FYI_REQ, KewApiConstants.ACTION_REQUEST_APPROVE_REQ, KewApiConstants.ACTION_REQUEST_COMPLETE_REQ })); 246 workflowDocument.route(annotation); 247 } 248 249 @Override 250 public void save(WorkflowDocument workflowDocument, String annotation) throws WorkflowException { 251 if (workflowDocument.isValidAction(ActionType.SAVE)) { 252 if (LOG.isDebugEnabled()) { 253 LOG.debug("saving document(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 254 } 255 256 workflowDocument.saveDocument(annotation); 257 } 258 else { 259 this.saveRoutingData(workflowDocument); 260 } 261 } 262 263 @Override 264 public void saveRoutingData(WorkflowDocument workflowDocument) throws WorkflowException { 265 if (LOG.isDebugEnabled()) { 266 LOG.debug("saving document(" + workflowDocument.getDocumentId() + ")"); 267 } 268 269 workflowDocument.saveDocumentData(); 270 } 271 272 @Override 273 public String getCurrentRouteLevelName(WorkflowDocument workflowDocument) throws WorkflowException { 274 if (LOG.isDebugEnabled()) { 275 LOG.debug("getting current route level name for document(" + workflowDocument.getDocumentId()); 276 } 277// return KEWServiceLocator.getRouteHeaderService().getRouteHeader(workflowDocument.getDocumentId()).getCurrentRouteLevelName(); 278 WorkflowDocument freshCopyWorkflowDoc = loadWorkflowDocument(workflowDocument.getDocumentId(), GlobalVariables.getUserSession().getPerson()); 279 return getCurrentRouteNodeNames(freshCopyWorkflowDoc); 280 } 281 282 283 284 @Override 285 public String getCurrentRouteNodeNames(WorkflowDocument workflowDocument) { 286 Set<String> nodeNames = workflowDocument.getNodeNames(); 287 if (nodeNames.isEmpty()) { 288 return ""; 289 } 290 StringBuilder builder = new StringBuilder(); 291 for (String nodeName : nodeNames) { 292 builder.append(nodeName).append(", "); 293 } 294 builder.setLength(builder.length() - 2); 295 return builder.toString(); 296 } 297 298 private void handleAdHocRouteRequests(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException { 299 handleAdHocRouteRequests(workflowDocument, annotation, adHocRecipients, null); 300 } 301 302 /** 303 * Convenience method for generating ad hoc requests for a given document 304 * 305 * @param flexDoc 306 * @param annotation 307 * @param adHocRecipients 308 * @throws InvalidActionTakenException 309 * @throws InvalidRouteTypeException 310 * @throws InvalidActionRequestException 311 */ 312 private void handleAdHocRouteRequests(WorkflowDocument workflowDocument, String annotation, List<AdHocRouteRecipient> adHocRecipients, String notificationLabel) throws WorkflowException { 313 314 if (adHocRecipients != null && adHocRecipients.size() > 0) { 315 String currentNode = null; 316 Set<String> currentNodes = workflowDocument.getNodeNames(); 317 if (currentNodes.isEmpty()) { 318 List<RouteNodeInstance> nodes = KewApiServiceLocator.getWorkflowDocumentService().getTerminalRouteNodeInstances( 319 workflowDocument.getDocumentId()); 320 currentNodes = new HashSet<String>(); 321 for (RouteNodeInstance node : nodes) { 322 currentNodes.add(node.getName()); 323 } 324 } 325 if (!currentNodes.isEmpty()) { 326 // for now just pick a node and go with it... 327 currentNode = currentNodes.iterator().next(); 328 } 329 330 List<AdHocRoutePerson> adHocRoutePersons = new ArrayList<AdHocRoutePerson>(); 331 List<AdHocRouteWorkgroup> adHocRouteWorkgroups = new ArrayList<AdHocRouteWorkgroup>(); 332 333 for (AdHocRouteRecipient recipient : adHocRecipients) { 334 if (StringUtils.isNotEmpty(recipient.getId())) { 335 String newAnnotation = annotation; 336 if ( StringUtils.isBlank( annotation ) ) { 337 try { 338 String message = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString( 339 RiceKeyConstants.MESSAGE_ADHOC_ANNOTATION); 340 newAnnotation = MessageFormat.format(message, GlobalVariables.getUserSession().getPrincipalName() ); 341 } catch ( Exception ex ) { 342 LOG.warn("Unable to set annotation", ex ); 343 } 344 } 345 if (AdHocRouteRecipient.PERSON_TYPE.equals(recipient.getType())) { 346 Principal principal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(recipient.getId()); 347 if (principal == null) { 348 throw new RiceRuntimeException("Could not locate principal with name '" + recipient.getId() + "'"); 349 } 350 workflowDocument.adHocToPrincipal(ActionRequestType.fromCode(recipient.getActionRequested()), currentNode, newAnnotation, principal.getPrincipalId(), "", true, notificationLabel); 351 AdHocRoutePerson personRecipient = (AdHocRoutePerson)recipient; 352 adHocRoutePersons.add(personRecipient); 353 } 354 else { 355 Group group = KimApiServiceLocator.getGroupService().getGroup(recipient.getId()); 356 if (group == null) { 357 throw new RiceRuntimeException("Could not locate group with id '" + recipient.getId() + "'"); 358 } 359 workflowDocument.adHocToGroup(ActionRequestType.fromCode(recipient.getActionRequested()), currentNode, newAnnotation, group.getId() , "", true, notificationLabel); 360 AdHocRouteWorkgroup groupRecipient = (AdHocRouteWorkgroup)recipient; 361 adHocRouteWorkgroups.add(groupRecipient); 362 } 363 } 364 } 365 366 for ( AdHocRoutePerson personRecipient : adHocRoutePersons ) { 367 dataObjectService.delete(personRecipient); 368 } 369 for ( AdHocRouteWorkgroup groupRecipient : adHocRouteWorkgroups ) { 370 dataObjectService.delete(groupRecipient); 371 } 372 } 373 } 374 375 /** 376 * Convenience method to filter out any ad hoc recipients that should not be allowed given the action requested of the user that 377 * is taking action on the document 378 * 379 * @param adHocRecipients 380 */ 381 private List<AdHocRouteRecipient> filterAdHocRecipients(List<AdHocRouteRecipient> adHocRecipients, String[] validTypes) { 382 // now filter out any but ack or fyi from the ad hoc list 383 List<AdHocRouteRecipient> realAdHocRecipients = new ArrayList<AdHocRouteRecipient>(); 384 if (adHocRecipients != null) { 385 for (AdHocRouteRecipient proposedRecipient : adHocRecipients) { 386 if (StringUtils.isNotBlank(proposedRecipient.getActionRequested())) { 387 for (int i = 0; i < validTypes.length; i++) { 388 if (validTypes[i].equals(proposedRecipient.getActionRequested())) { 389 realAdHocRecipients.add(proposedRecipient); 390 } 391 } 392 } 393 } 394 } 395 return realAdHocRecipients; 396 } 397 398 /** 399 * Completes workflow document 400 * 401 * @see WorkflowDocumentService#complete(org.kuali.rice.kew.api.WorkflowDocument, String, java.util.List) 402 */ 403 @Override 404 public void complete(WorkflowDocument workflowDocument, String annotation, List adHocRecipients) throws WorkflowException { 405 if (LOG.isDebugEnabled()) { 406 LOG.debug("routing flexDoc(" + workflowDocument.getDocumentId() + ",'" + annotation + "')"); 407 } 408 handleAdHocRouteRequests(workflowDocument, annotation, filterAdHocRecipients(adHocRecipients, new String[] { KewApiConstants.ACTION_REQUEST_COMPLETE_REQ,KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, KewApiConstants.ACTION_REQUEST_FYI_REQ, KewApiConstants.ACTION_REQUEST_APPROVE_REQ })); 409 workflowDocument.complete(annotation); 410 } 411 412 public void setDataObjectService(DataObjectService dataObjectService) { 413 this.dataObjectService = dataObjectService; 414 } 415}