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