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}