View Javadoc
1   /**
2    * Copyright 2005-2015 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.krad.workflow.service.impl;
17  
18  import java.text.MessageFormat;
19  import java.util.ArrayList;
20  import java.util.HashSet;
21  import java.util.List;
22  import java.util.Set;
23  
24  import org.apache.commons.lang.StringUtils;
25  import org.apache.commons.lang.time.StopWatch;
26  import org.kuali.rice.core.api.CoreApiServiceLocator;
27  import org.kuali.rice.core.api.exception.RiceRuntimeException;
28  import org.kuali.rice.core.api.util.RiceKeyConstants;
29  import org.kuali.rice.kew.api.KewApiConstants;
30  import org.kuali.rice.kew.api.KewApiServiceLocator;
31  import org.kuali.rice.kew.api.WorkflowDocument;
32  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
33  import org.kuali.rice.kew.api.action.ActionRequestType;
34  import org.kuali.rice.kew.api.action.ActionType;
35  import org.kuali.rice.kew.api.document.node.RouteNodeInstance;
36  import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
37  import org.kuali.rice.kew.api.exception.WorkflowException;
38  import org.kuali.rice.kim.api.group.Group;
39  import org.kuali.rice.kim.api.identity.Person;
40  import org.kuali.rice.kim.api.identity.principal.Principal;
41  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
42  import org.kuali.rice.krad.bo.AdHocRoutePerson;
43  import org.kuali.rice.krad.bo.AdHocRouteRecipient;
44  import org.kuali.rice.krad.bo.AdHocRouteWorkgroup;
45  import org.kuali.rice.krad.data.DataObjectService;
46  import org.kuali.rice.krad.exception.UnknownDocumentIdException;
47  import org.kuali.rice.krad.util.GlobalVariables;
48  import org.kuali.rice.krad.workflow.service.WorkflowDocumentService;
49  import org.springframework.transaction.annotation.Transactional;
50  
51  
52  /**
53   * Implementation of the WorkflowDocumentService, which makes use of Workflow
54   *
55   * @author Kuali Rice Team (rice.collab@kuali.org)
56   */
57  @Transactional
58  public class WorkflowDocumentServiceImpl implements WorkflowDocumentService {
59      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(WorkflowDocumentServiceImpl.class);
60  
61      protected DataObjectService dataObjectService;
62  
63      @Override
64      public boolean workflowDocumentExists(String documentId) {
65          boolean exists = false;
66  
67          if (StringUtils.isBlank(documentId)) {
68              throw new IllegalArgumentException("invalid (blank) documentId");
69          }
70  
71          exists = KewApiServiceLocator.getWorkflowDocumentService().doesDocumentExist(documentId);
72  
73          return exists;
74      }
75  
76      @Override
77      public WorkflowDocument createWorkflowDocument(String documentTypeName, Person person) {
78          String watchName = "createWorkflowDocument";
79          StopWatch watch = null;
80          if (LOG.isDebugEnabled()) {
81          	watch = new StopWatch();
82              watch.start();
83              LOG.debug(watchName + ": started");
84          }
85          if (StringUtils.isBlank(documentTypeName)) {
86              throw new IllegalArgumentException("invalid (blank) documentTypeName");
87          }
88          if (person == null) {
89              throw new IllegalArgumentException("invalid (null) person");
90          }
91  
92          if (StringUtils.isBlank(person.getPrincipalName())) {
93              throw new IllegalArgumentException("invalid (empty) PrincipalName");
94          }
95  
96          if (LOG.isDebugEnabled()) {
97              LOG.debug("creating workflowDoc(" + documentTypeName + "," + person.getPrincipalName() + ")");
98          }
99  
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 }