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