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}