001/**
002 * Copyright 2005-2018 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.document;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang.ArrayUtils;
020import org.apache.commons.lang.StringUtils;
021import org.kuali.rice.core.api.CoreApiServiceLocator;
022import org.kuali.rice.core.api.config.property.ConfigContext;
023import org.kuali.rice.core.api.config.property.ConfigurationService;
024import org.kuali.rice.core.api.exception.RiceRuntimeException;
025import org.kuali.rice.core.api.util.RiceKeyConstants;
026import org.kuali.rice.coreservice.framework.parameter.ParameterConstants;
027import org.kuali.rice.coreservice.framework.parameter.ParameterService;
028import org.kuali.rice.kew.api.KewApiConstants;
029import org.kuali.rice.kew.api.KewApiServiceLocator;
030import org.kuali.rice.kew.api.WorkflowDocument;
031import org.kuali.rice.kew.api.action.ActionRequest;
032import org.kuali.rice.kew.api.action.ActionRequestType;
033import org.kuali.rice.kew.api.action.DocumentActionParameters;
034import org.kuali.rice.kew.api.action.WorkflowDocumentActionsService;
035import org.kuali.rice.kew.api.doctype.DocumentType;
036import org.kuali.rice.kew.api.exception.WorkflowException;
037import org.kuali.rice.kim.api.identity.Person;
038import org.kuali.rice.krad.UserSessionUtils;
039import org.kuali.rice.krad.bo.AdHocRouteRecipient;
040import org.kuali.rice.krad.bo.Attachment;
041import org.kuali.rice.krad.bo.DocumentHeader;
042import org.kuali.rice.krad.bo.Note;
043import org.kuali.rice.krad.exception.DocumentAuthorizationException;
044import org.kuali.rice.krad.exception.UnknownDocumentIdException;
045import org.kuali.rice.krad.exception.ValidationException;
046import org.kuali.rice.krad.maintenance.MaintenanceDocument;
047import org.kuali.rice.krad.rules.rule.event.AddNoteEvent;
048import org.kuali.rice.krad.rules.rule.event.DocumentEvent;
049import org.kuali.rice.krad.rules.rule.event.RouteDocumentEvent;
050import org.kuali.rice.krad.rules.rule.event.SaveDocumentEvent;
051import org.kuali.rice.krad.service.AttachmentService;
052import org.kuali.rice.krad.service.DataDictionaryService;
053import org.kuali.rice.krad.service.DocumentDictionaryService;
054import org.kuali.rice.krad.service.DocumentService;
055import org.kuali.rice.krad.service.KRADServiceLocator;
056import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
057import org.kuali.rice.krad.service.LegacyDataAdapter;
058import org.kuali.rice.krad.service.NoteService;
059import org.kuali.rice.krad.uif.UifConstants;
060import org.kuali.rice.krad.uif.UifParameters;
061import org.kuali.rice.krad.uif.UifPropertyPaths;
062import org.kuali.rice.krad.uif.component.BindingInfo;
063import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
064import org.kuali.rice.krad.uif.view.DocumentView;
065import org.kuali.rice.krad.util.GlobalVariables;
066import org.kuali.rice.krad.util.KRADConstants;
067import org.kuali.rice.krad.util.KRADUtils;
068import org.kuali.rice.krad.util.NoteType;
069import org.kuali.rice.krad.web.form.DialogResponse;
070import org.kuali.rice.krad.web.form.DocumentFormBase;
071import org.kuali.rice.krad.web.form.UifFormBase;
072import org.kuali.rice.krad.web.service.CollectionControllerService;
073import org.kuali.rice.krad.web.service.ModelAndViewService;
074import org.kuali.rice.krad.web.service.NavigationControllerService;
075import org.kuali.rice.krad.web.service.impl.ControllerServiceImpl;
076import org.kuali.rice.ksb.api.KsbApiServiceLocator;
077import org.springframework.web.multipart.MultipartFile;
078import org.springframework.web.servlet.ModelAndView;
079
080import javax.servlet.http.HttpServletResponse;
081import javax.xml.namespace.QName;
082import java.io.IOException;
083import java.util.ArrayList;
084import java.util.List;
085import java.util.Properties;
086import java.util.Set;
087
088/**
089 * Default implementation of the document controller service.
090 *
091 * @author Kuali Rice Team (rice.collab@kuali.org)
092 */
093public class DocumentControllerServiceImpl extends ControllerServiceImpl implements DocumentControllerService {
094    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
095            DocumentControllerServiceImpl.class);
096
097    // COMMAND constants which cause docHandler to load an existing document instead of creating a new one
098    protected static final String[] DOCUMENT_LOAD_COMMANDS =
099            {KewApiConstants.ACTIONLIST_COMMAND, KewApiConstants.DOCSEARCH_COMMAND, KewApiConstants.SUPERUSER_COMMAND,
100                    KewApiConstants.HELPDESK_ACTIONLIST_COMMAND};
101    protected static final String SENSITIVE_DATA_DIALOG = "DialogGroup-SensitiveData";
102    protected static final String EXPLANATION_DIALOG = "DisapproveExplanationDialog";
103
104    private LegacyDataAdapter legacyDataAdapter;
105    private DataDictionaryService dataDictionaryService;
106    private DocumentService documentService;
107    private DocumentDictionaryService documentDictionaryService;
108    private AttachmentService attachmentService;
109    private NoteService noteService;
110    private ModelAndViewService modelAndViewService;
111    private NavigationControllerService navigationControllerService;
112    private ConfigurationService configurationService;
113    private CollectionControllerService collectionControllerService;
114    private ParameterService parameterService;
115
116    /**
117     * Determines whether a new document instance needs created or we need to load an existing document by
118     * checking the {@link org.kuali.rice.krad.web.form.DocumentFormBase#getCommand()} value, then delegates to
119     * a helper method to carry out the action.
120     *
121     * {@inheritDoc}
122     */
123    @Override
124    public ModelAndView docHandler(DocumentFormBase form) throws WorkflowException {
125        String command = form.getCommand();
126        DocumentView view = (DocumentView) form.getView();
127
128        if (ArrayUtils.contains(DOCUMENT_LOAD_COMMANDS, command) && (form.getDocId() != null)) {
129            checkReturnLocationForDocSearch(command, form);
130
131            loadDocument(form);
132
133            if (KewApiConstants.SUPERUSER_COMMAND.equals(command)) {
134                view.setSuperUserView(true);
135            }
136        } else if (KewApiConstants.INITIATE_COMMAND.equals(command)) {
137            if (view != null) {
138                form.setApplyDefaultValues(true);
139            }
140
141            createDocument(form);
142        } else {
143            LOG.error("docHandler called with invalid parameters");
144            throw new IllegalArgumentException("docHandler called with invalid parameters");
145        }
146
147        return getModelAndViewService().getModelAndView(form);
148    }
149
150    /**
151     * Determines if the DOCSEARCH_COMMAND is the present command value and updates the return location to the base
152     * application url
153     *
154     * @param command the current command value
155     * @param form the form with the updated return location
156     */
157    private void checkReturnLocationForDocSearch(String command, DocumentFormBase form) {
158        if (KewApiConstants.DOCSEARCH_COMMAND.equals(command)) {
159            form.setReturnLocation(ConfigContext.getCurrentContextConfig().getProperty(KRADConstants.APPLICATION_URL_KEY));
160        }
161    }
162
163    /**
164     * Loads the document by its provided document header id on the given form.
165     *
166     * <p>This has been abstracted out so that it can be overridden in children if the need arises</p>
167     *
168     * @param form form instance that contains the doc id parameter and where
169     * the retrieved document instance should be set
170     */
171    protected void loadDocument(DocumentFormBase form) throws WorkflowException {
172        String docId = form.getDocId();
173
174        if (LOG.isDebugEnabled()) {
175            LOG.debug("Loading document" + docId);
176        }
177
178        Document document = getDocumentService().getByDocumentHeaderId(docId);
179        if (document == null) {
180            throw new UnknownDocumentIdException(
181                    "Document no longer exists.  It may have been cancelled before being saved.");
182        }
183
184        WorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument();
185        if (!getDocumentDictionaryService().getDocumentAuthorizer(document).canOpen(document,
186                GlobalVariables.getUserSession().getPerson())) {
187            throw buildAuthorizationException("open", document);
188        }
189
190        // re-retrieve the document using the current user's session - remove
191        // the system user from the WorkflowDcument object
192        if (workflowDocument != document.getDocumentHeader().getWorkflowDocument()) {
193            LOG.warn("Workflow document changed via canOpen check");
194            document.getDocumentHeader().setWorkflowDocument(workflowDocument);
195        }
196
197        form.setDocument(document);
198        form.setDocTypeName(workflowDocument.getDocumentTypeName());
199
200        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(), workflowDocument);
201    }
202
203    /**
204     * Creates a new document of the type specified by the docTypeName property of the given form.
205     *
206     * <p>This has been abstracted out so that it can be overridden in children if the need arises</p>
207     *
208     * @param form form instance that contains the doc type parameter and where
209     * the new document instance should be set
210     */
211    protected void createDocument(DocumentFormBase form) throws WorkflowException {
212        if (LOG.isDebugEnabled()) {
213            LOG.debug("Creating new document instance for doc type: " + form.getDocTypeName());
214        }
215
216        Document doc = getDocumentService().getNewDocument(form.getDocTypeName());
217
218        form.setDocument(doc);
219        form.setDocTypeName(doc.getDocumentHeader().getWorkflowDocument().getDocumentTypeName());
220    }
221
222    /**
223     * {@inheritDoc}
224     */
225    @Override
226    public ModelAndView cancel(UifFormBase form) {
227        performWorkflowAction((DocumentFormBase) form, UifConstants.WorkflowAction.CANCEL);
228
229        return getNavigationControllerService().returnToPrevious(form);
230    }
231
232    /**
233     * {@inheritDoc}
234     */
235    @Override
236    public ModelAndView reload(DocumentFormBase form) throws WorkflowException {
237        Document document = form.getDocument();
238
239        // prepare the reload action by calling dochandler (set doc id and command)
240        form.setDocId(document.getDocumentNumber());
241        form.setCommand(DOCUMENT_LOAD_COMMANDS[1]);
242
243        form.setEvaluateFlagsAndModes(true);
244        form.setCanEditView(null);
245
246        GlobalVariables.getMessageMap().putInfo(KRADConstants.GLOBAL_MESSAGES, RiceKeyConstants.MESSAGE_RELOADED);
247
248        return docHandler(form);
249    }
250
251    /**
252     * {@inheritDoc}
253     */
254    @Override
255    public ModelAndView recall(DocumentFormBase form) {
256        performWorkflowAction(form, UifConstants.WorkflowAction.RECALL);
257
258        return getModelAndViewService().getModelAndView(form);
259    }
260
261    /**
262     * {@inheritDoc}
263     */
264    @Override
265    public ModelAndView save(DocumentFormBase form) {
266        return save(form, null);
267    }
268
269    /**
270     * {@inheritDoc}
271     */
272    @Override
273    public ModelAndView save(DocumentFormBase form, SaveDocumentEvent saveDocumentEvent) {
274        Document document = form.getDocument();
275
276        // get the explanation from the document and check it for sensitive data
277        String explanation = document.getDocumentHeader().getExplanation();
278        ModelAndView sensitiveDataDialogModelAndView = checkSensitiveDataAndWarningDialog(explanation, form);
279
280        // if a sensitive data warning dialog is returned then display it
281        if (sensitiveDataDialogModelAndView != null) {
282            return sensitiveDataDialogModelAndView;
283        }
284
285        performWorkflowAction(form, UifConstants.WorkflowAction.SAVE, saveDocumentEvent);
286
287        return getModelAndViewService().getModelAndView(form);
288    }
289
290    /**
291     * {@inheritDoc}
292     */
293    @Override
294    public ModelAndView complete(DocumentFormBase form) {
295        performWorkflowAction(form, UifConstants.WorkflowAction.COMPLETE);
296
297        return getModelAndViewService().getModelAndView(form);
298    }
299
300    /**
301     * {@inheritDoc}
302     */
303    @Override
304    public ModelAndView route(DocumentFormBase form) {
305        performWorkflowAction(form, UifConstants.WorkflowAction.ROUTE);
306
307        return getModelAndViewService().getModelAndView(form);
308    }
309
310    /**
311     * {@inheritDoc}
312     */
313    @Override
314    public ModelAndView blanketApprove(DocumentFormBase form) {
315        Document document = form.getDocument();
316        WorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument();
317
318        //disable blanket approve if adhoc route completion exists
319        List<AdHocRouteRecipient> adHocRecipients = new ArrayList<AdHocRouteRecipient>();
320        adHocRecipients.addAll(document.getAdHocRoutePersons());
321        adHocRecipients.addAll(document.getAdHocRouteWorkgroups());
322
323        //check for ad hoc completion request
324        for (AdHocRouteRecipient adHocRouteRecipient : adHocRecipients) {
325            String actionRequestedCode = adHocRouteRecipient.getActionRequested();
326
327            //add error and send back
328            if (KewApiConstants.ACTION_REQUEST_COMPLETE_REQ.equals(actionRequestedCode)) {
329                GlobalVariables.getMessageMap().putError(KRADConstants.NEW_AD_HOC_ROUTE_WORKGROUP_PROPERTY_NAME,
330                        RiceKeyConstants.ERROR_ADHOC_COMPLETE_BLANKET_APPROVE_NOT_ALLOWED);
331
332                return getModelAndViewService().getModelAndView(form);
333            }
334        }
335
336        performWorkflowAction(form, UifConstants.WorkflowAction.BLANKETAPPROVE);
337
338        if (GlobalVariables.getMessageMap().hasErrors()) {
339            return getModelAndViewService().getModelAndView(form);
340        }
341
342        return getNavigationControllerService().returnToPrevious(form);
343    }
344
345    /**
346     * {@inheritDoc}
347     */
348    @Override
349    public ModelAndView approve(DocumentFormBase form) {
350        performWorkflowAction(form, UifConstants.WorkflowAction.APPROVE);
351
352        return getNavigationControllerService().returnToPrevious(form);
353    }
354
355    /**
356     * {@inheritDoc}
357     */
358    @Override
359    public ModelAndView disapprove(DocumentFormBase form) {
360        // get the explanation for disapproval from the disapprove dialog and check it for sensitive data
361        String explanationData = generateDisapprovalNote(form);
362        ModelAndView sensitiveDataDialogModelAndView = checkSensitiveDataAndWarningDialog(explanationData, form);
363
364        // if a sensitive data warning dialog is returned then display it
365        if (sensitiveDataDialogModelAndView != null) {
366            return sensitiveDataDialogModelAndView;
367        }
368
369        performWorkflowAction(form, UifConstants.WorkflowAction.DISAPPROVE);
370
371        return getNavigationControllerService().returnToPrevious(form);
372    }
373
374    /**
375     * Convenience method for generating disapproval note with text from the explanation dialog.
376     *
377     * @param form document form instance containing the explanation dialog
378     * @return
379     */
380    protected String generateDisapprovalNote(DocumentFormBase form) {
381        String explanationData = form.getDialogExplanations().get(EXPLANATION_DIALOG);
382        if(explanationData == null) {
383            return "";
384        }
385
386        // build out full message to return
387        String introNoteMessage = getConfigurationService().getPropertyValueAsString(
388                RiceKeyConstants.MESSAGE_DISAPPROVAL_NOTE_TEXT_INTRO) + KRADConstants.BLANK_SPACE;
389
390        return introNoteMessage + explanationData;
391    }
392
393    /**
394     * {@inheritDoc}
395     */
396    @Override
397    public ModelAndView fyi(DocumentFormBase form) {
398        performWorkflowAction(form, UifConstants.WorkflowAction.FYI);
399
400        return getNavigationControllerService().returnToPrevious(form);
401    }
402
403    /**
404     * {@inheritDoc}
405     */
406    @Override
407    public ModelAndView acknowledge(DocumentFormBase form) {
408        performWorkflowAction(form, UifConstants.WorkflowAction.ACKNOWLEDGE);
409
410        return getNavigationControllerService().returnToPrevious(form);
411    }
412
413    /**
414     * {@inheritDoc}
415     */
416    @Override
417    public ModelAndView sendAdHocRequests(DocumentFormBase form) {
418        performWorkflowAction(form, UifConstants.WorkflowAction.SENDADHOCREQUESTS);
419
420        return getModelAndViewService().getModelAndView(form);
421    }
422
423    /**
424     * {@inheritDoc}
425     */
426    @Override
427    public ModelAndView supervisorFunctions(DocumentFormBase form) {
428        String workflowSuperUserUrl = getConfigurationService().getPropertyValueAsString(KRADConstants.WORKFLOW_URL_KEY)
429                + "/"
430                + KRADConstants.SUPERUSER_ACTION;
431
432        Properties props = new Properties();
433        props.setProperty(UifParameters.METHOD_TO_CALL, UifConstants.MethodToCallNames.DISPLAY_SUPER_USER_DOCUMENT);
434        props.setProperty(UifPropertyPaths.DOCUMENT_ID, form.getDocument().getDocumentNumber());
435
436        return getModelAndViewService().performRedirect(form, workflowSuperUserUrl, props);
437    }
438
439    /**
440     * {@inheritDoc}
441     */
442    @Override
443    public ModelAndView close(DocumentFormBase form) {
444        return getNavigationControllerService().returnToPrevious(form);
445    }
446
447    /**
448     * Validates the note, saves attachment, adds the time stamp and author, and calls the
449     * generic addLine method.
450     *
451     * {@inheritDoc}
452     */
453    @Override
454    public ModelAndView insertNote(DocumentFormBase form) {
455        Document document = form.getDocument();
456
457        Note newNote = getAddLineNoteInstance(form);
458        setNewNoteProperties(form, document, newNote);
459
460        Attachment attachment = getNewNoteAttachment(form, document, newNote);
461
462        // validate the note
463        boolean rulesPassed = KRADServiceLocatorWeb.getKualiRuleService().applyRules(new AddNoteEvent(document,
464                newNote));
465        if (!rulesPassed) {
466            return getModelAndViewService().getModelAndView(form);
467        }
468
469        // adding the attachment after refresh gets called, since the attachment record doesn't get persisted
470        // until the note does (and therefore refresh doesn't have any attachment to autoload based on the id, nor
471        // does it autopopulate the id since the note hasn't been persisted yet)
472        if (attachment != null) {
473            newNote.addAttachment(attachment);
474        }
475
476        // check for sensitive data within the note and display warning dialog if necessary
477        ModelAndView sensitiveDataDialogModelAndView = checkSensitiveDataAndWarningDialog(newNote.getNoteText(), form);
478        if (sensitiveDataDialogModelAndView != null) {
479            return sensitiveDataDialogModelAndView;
480        }
481
482        saveNewNote(form, document, newNote);
483
484        return getCollectionControllerService().addLine(form);
485    }
486
487    /**
488     * Retrieves the note instance on the form that should be added to the document notes.
489     *
490     * @param form form instance containing the add note instance
491     * @return
492     */
493    protected Note getAddLineNoteInstance(DocumentFormBase form) {
494        String selectedCollectionId = form.getActionParamaterValue(UifParameters.SELECTED_COLLECTION_ID);
495
496        BindingInfo addLineBindingInfo = (BindingInfo) form.getViewPostMetadata().getComponentPostData(
497                selectedCollectionId, UifConstants.PostMetadata.ADD_LINE_BINDING_INFO);
498
499        String addLinePath = addLineBindingInfo.getBindingPath();
500        Object addLine = ObjectPropertyUtils.getPropertyValue(form, addLinePath);
501
502        return (Note) addLine;
503    }
504
505    /**
506     * Defaults properties (posted timestamp, object id, author) on the note instance that will be added.
507     *
508     * @param form form instance containing the add note instance
509     * @param document document instance the note will be added to
510     * @param newNote note instance to set properties on
511     */
512    protected void setNewNoteProperties(DocumentFormBase form, Document document, Note newNote) {
513        newNote.setNotePostedTimestampToCurrent();
514        newNote.setRemoteObjectIdentifier(document.getNoteTarget().getObjectId());
515
516        Person kualiUser = GlobalVariables.getUserSession().getPerson();
517        if (kualiUser == null) {
518            throw new IllegalStateException("Current UserSession has a null Person.");
519        }
520
521        newNote.setAuthorUniversalIdentifier(kualiUser.getPrincipalId());
522    }
523
524    /**
525     * Builds an attachment for the file (if any) associated with the add note instance.
526     *
527     * @param form form instance containing the attachment file
528     * @param document document instance the attachment should be associated with
529     * @param newNote note instance the attachment should be associated with
530     * @return Attachment instance for the note, or null if no attachment file was present
531     */
532    protected Attachment getNewNoteAttachment(DocumentFormBase form, Document document, Note newNote) {
533        MultipartFile attachmentFile = form.getAttachmentFile();
534
535        if ((attachmentFile == null) || StringUtils.isBlank(attachmentFile.getOriginalFilename())) {
536            return null;
537        }
538
539        if (attachmentFile.getSize() == 0) {
540            GlobalVariables.getMessageMap().putError(String.format("%s.%s",
541                    KRADConstants.NEW_DOCUMENT_NOTE_PROPERTY_NAME, KRADConstants.NOTE_ATTACHMENT_FILE_PROPERTY_NAME),
542                    RiceKeyConstants.ERROR_UPLOADFILE_EMPTY, attachmentFile.getOriginalFilename());
543
544            return null;
545        }
546
547        String attachmentTypeCode = null;
548        if (newNote.getAttachment() != null) {
549            attachmentTypeCode = newNote.getAttachment().getAttachmentTypeCode();
550        }
551
552        DocumentAuthorizer documentAuthorizer = getDocumentDictionaryService().getDocumentAuthorizer(document);
553        if (!documentAuthorizer.canAddNoteAttachment(document, attachmentTypeCode,
554                GlobalVariables.getUserSession().getPerson())) {
555            throw buildAuthorizationException("annotate", document);
556        }
557
558        Attachment attachment;
559        try {
560            attachment = getAttachmentService().createAttachment(document.getNoteTarget(),
561                    attachmentFile.getOriginalFilename(), attachmentFile.getContentType(),
562                    (int) attachmentFile.getSize(), attachmentFile.getInputStream(), attachmentTypeCode);
563        } catch (IOException e) {
564            throw new RiceRuntimeException("Unable to store attachment", e);
565        }
566
567        return attachment;
568    }
569
570    /**
571     * Saves a new note instance to the data store if the document state allows it.
572     *
573     * @param form form instance containing the add note instance
574     * @param document document instance the note is associated with
575     * @param newNote note instance to save
576     */
577    protected void saveNewNote(DocumentFormBase form, Document document, Note newNote) {
578        DocumentHeader documentHeader = document.getDocumentHeader();
579
580        if (!documentHeader.getWorkflowDocument().isInitiated() && StringUtils.isNotEmpty(
581                document.getNoteTarget().getObjectId()) && !(document instanceof MaintenanceDocument && NoteType
582                .BUSINESS_OBJECT.getCode().equals(newNote.getNoteTypeCode()))) {
583
584            getNoteService().save(newNote);
585        }
586    }
587
588    /**
589     * {@inheritDoc}
590     */
591    @Override
592    public ModelAndView deleteNote(DocumentFormBase form) {
593        String selectedLineIndex = form.getActionParamaterValue(UifParameters.SELECTED_LINE_INDEX);
594
595        Document document = form.getDocument();
596
597        Note note = document.getNote(Integer.parseInt(selectedLineIndex));
598        Attachment attachment = note.getAttachment();
599
600        String attachmentTypeCode = null;
601        if (attachment != null) {
602            attachmentTypeCode = attachment.getAttachmentTypeCode();
603        }
604
605        // verify the user has permissions to delete the note
606        Person user = GlobalVariables.getUserSession().getPerson();
607        if (!getDocumentDictionaryService().getDocumentAuthorizer(document).canDeleteNoteAttachment(document,
608                attachmentTypeCode, note.getAuthorUniversalIdentifier(), user)) {
609            throw buildAuthorizationException("annotate", document);
610        }
611
612        if (attachment != null && attachment.isComplete()) {
613            getAttachmentService().deleteAttachmentContents(attachment);
614        }
615
616        // if document is not saved there is no need to delete the note (it is not persisted)
617        if (!document.getDocumentHeader().getWorkflowDocument().isInitiated()) {
618            getNoteService().deleteNote(note);
619        }
620
621        return getCollectionControllerService().deleteLine(form);
622    }
623
624    /**
625     * Retrieves a note attachment by either the line index of the note within the documents note collection, or
626     * by the note identifier.
627     *
628     * {@inheritDoc}
629     */
630    @Override
631    public ModelAndView downloadAttachment(DocumentFormBase form, HttpServletResponse response) {
632        Attachment attachment = null;
633
634        String selectedLineIndex = form.getActionParamaterValue(UifParameters.SELECTED_LINE_INDEX);
635        if (StringUtils.isNotBlank(selectedLineIndex)) {
636            Note note = form.getDocument().getNote(Integer.parseInt(selectedLineIndex));
637            attachment = note.getAttachment();
638        } else {
639            Long noteIdentifier = Long.valueOf(form.getActionParamaterValue(KRADConstants.NOTE_IDENTIFIER));
640            Note note = getNoteService().getNoteByNoteId(noteIdentifier);
641            if ((note != null) && (note.getAttachment() != null)) {
642                attachment = note.getAttachment();
643
644                // make sure the reference back to note is set for the note service dependencies
645                attachment.setNote(note);
646            }
647        }
648
649        if (attachment == null) {
650            throw new RuntimeException("Unable to find attachment for action parameters passed.");
651        }
652
653        try {
654            KRADUtils.addAttachmentToResponse(response, getAttachmentService().retrieveAttachmentContents(attachment),
655                    attachment.getAttachmentMimeTypeCode(), attachment.getAttachmentFileName(),
656                    attachment.getAttachmentFileSize().longValue());
657        } catch (IOException e) {
658            throw new RuntimeException("Unable to download note attachment", e);
659        }
660
661        return null;
662    }
663
664    /**
665     * {@inheritDoc}
666     */
667    @Override
668    public ModelAndView cancelAttachment(DocumentFormBase form) {
669        form.setAttachmentFile(null);
670
671        return getModelAndViewService().getModelAndView(form);
672    }
673
674    /**
675     * {@inheritDoc}
676     */
677    @Override
678    public ModelAndView superUserTakeActions(DocumentFormBase form) {
679        Document document = form.getDocument();
680
681        if (StringUtils.isBlank(document.getSuperUserAnnotation())) {
682            GlobalVariables.getMessageMap().putErrorForSectionId(
683                    "Uif-SuperUserAnnotation", RiceKeyConstants.ERROR_SUPER_USER_TAKE_ACTIONS_MISSING);
684        }
685
686        Set<String> selectedActionRequests = form.getSelectedCollectionLines().get(UifPropertyPaths.ACTION_REQUESTS);
687
688        if (CollectionUtils.isEmpty(selectedActionRequests)) {
689            GlobalVariables.getMessageMap().putErrorForSectionId(
690                    "Uif-SuperUserActionRequests", RiceKeyConstants.ERROR_SUPER_USER_TAKE_ACTIONS_NONE_SELECTED);
691        }
692
693        if (GlobalVariables.getMessageMap().hasErrors()) {
694            return getModelAndViewService().getModelAndView(form);
695        }
696
697        List<ActionRequest> actionRequests = new ArrayList<ActionRequest>();
698        for (String selectedActionRequest : selectedActionRequests) {
699            ActionRequest actionRequest = ObjectPropertyUtils.getPropertyValue(document, selectedActionRequest);
700            actionRequests.add(actionRequest);
701        }
702
703        for (ActionRequest actionRequest : actionRequests) {
704            if (StringUtils.equals(actionRequest.getActionRequested().getCode(), ActionRequestType.COMPLETE.getCode()) ||
705                    StringUtils.equals(actionRequest.getActionRequested().getCode(), ActionRequestType.APPROVE.getCode())) {
706                document = getDocumentService().validateAndPersistDocument(document, new RouteDocumentEvent(document));
707                form.setDocument(document);
708            }
709
710            performSuperUserWorkflowAction(form, UifConstants.SuperUserWorkflowAction.TAKEACTION, actionRequest);
711        }
712
713        document.setSuperUserAnnotation("");
714        form.getSelectedCollectionLines().remove(UifPropertyPaths.ACTION_REQUESTS);
715
716        return getModelAndViewService().getModelAndView(form);
717    }
718
719    /**
720     * {@inheritDoc}
721     */
722    @Override
723    public ModelAndView superUserApprove(DocumentFormBase form) {
724        Document document = form.getDocument();
725
726        if (StringUtils.isBlank(document.getSuperUserAnnotation())) {
727            GlobalVariables.getMessageMap().putErrorForSectionId(
728                    "Uif-SuperUserAnnotation", RiceKeyConstants.ERROR_SUPER_USER_APPROVE_MISSING);
729        }
730
731        Set<String> selectedCollectionLines = form.getSelectedCollectionLines().get(UifPropertyPaths.ACTION_REQUESTS);
732
733        if (!CollectionUtils.isEmpty(selectedCollectionLines)) {
734            GlobalVariables.getMessageMap().putErrorForSectionId(
735                    "Uif-SuperUserActionRequests", RiceKeyConstants.ERROR_SUPER_USER_APPROVE_ACTIONS_CHECKED);
736        }
737
738        if (GlobalVariables.getMessageMap().hasErrors()) {
739            return getModelAndViewService().getModelAndView(form);
740        }
741
742        performSuperUserWorkflowAction(form, UifConstants.SuperUserWorkflowAction.APPROVE);
743
744        return getModelAndViewService().getModelAndView(form);
745    }
746
747    /**
748     * {@inheritDoc}
749     */
750    @Override
751    public ModelAndView superUserDisapprove(DocumentFormBase form) {
752        Document document = form.getDocument();
753
754        if (StringUtils.isBlank(document.getSuperUserAnnotation())) {
755            GlobalVariables.getMessageMap().putErrorForSectionId(
756                    "Uif-SuperUserAnnotation", RiceKeyConstants.ERROR_SUPER_USER_DISAPPROVE_MISSING);
757        }
758
759        Set<String> selectedCollectionLines = form.getSelectedCollectionLines().get(UifPropertyPaths.ACTION_REQUESTS);
760
761        if (!CollectionUtils.isEmpty(selectedCollectionLines)) {
762            GlobalVariables.getMessageMap().putErrorForSectionId(
763                    "Uif-SuperUserActionRequests", RiceKeyConstants.ERROR_SUPER_USER_DISAPPROVE_ACTIONS_CHECKED);
764        }
765
766        if (GlobalVariables.getMessageMap().hasErrors()) {
767            return getModelAndViewService().getModelAndView(form);
768        }
769
770        performSuperUserWorkflowAction(form, UifConstants.SuperUserWorkflowAction.DISAPPROVE);
771
772        return getModelAndViewService().getModelAndView(form);
773    }
774
775    /**
776     * {@inheritDoc}
777     */
778    @Override
779    public void performWorkflowAction(DocumentFormBase form, UifConstants.WorkflowAction action) {
780        performWorkflowAction(form, action, null);
781    }
782
783    /**
784     * {@inheritDoc}
785     */
786    @Override
787    public void performWorkflowAction(DocumentFormBase form, UifConstants.WorkflowAction action,
788            DocumentEvent documentEvent) {
789        Document document = form.getDocument();
790
791        if (LOG.isDebugEnabled()) {
792            LOG.debug("Performing workflow action " + action.name() + "for document: " + document.getDocumentNumber());
793        }
794
795        // evaluate flags on save only if we are saving for the first time (transitioning to saved status)
796        if (!UifConstants.WorkflowAction.SAVE.equals(action) || document.getDocumentHeader().getWorkflowDocument()
797                .isInitiated()) {
798            form.setEvaluateFlagsAndModes(true);
799            form.setCanEditView(null);
800        }
801
802        try {
803            String successMessageKey = null;
804            switch (action) {
805                case SAVE:
806                    if (documentEvent == null) {
807                        document = getDocumentService().saveDocument(document);
808                    } else {
809                        document = getDocumentService().saveDocument(document, documentEvent);
810                    }
811
812                    successMessageKey = RiceKeyConstants.MESSAGE_SAVED;
813                    break;
814                case ROUTE:
815                    document = getDocumentService().routeDocument(document, form.getAnnotation(),
816                            combineAdHocRecipients(form));
817                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_SUCCESSFUL;
818                    break;
819                case BLANKETAPPROVE:
820                    document = getDocumentService().blanketApproveDocument(document, form.getAnnotation(),
821                            combineAdHocRecipients(form));
822                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_APPROVED;
823                    break;
824                case APPROVE:
825                    document = getDocumentService().approveDocument(document, form.getAnnotation(),
826                            combineAdHocRecipients(form));
827                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_APPROVED;
828                    break;
829                case DISAPPROVE:
830                    String disapprovalNoteText = generateDisapprovalNote(form);
831                    document = getDocumentService().disapproveDocument(document, disapprovalNoteText);
832                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_DISAPPROVED;
833                    break;
834                case FYI:
835                    document = getDocumentService().clearDocumentFyi(document, combineAdHocRecipients(form));
836                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_FYIED;
837                    break;
838                case ACKNOWLEDGE:
839                    document = getDocumentService().acknowledgeDocument(document, form.getAnnotation(),
840                            combineAdHocRecipients(form));
841                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_ACKNOWLEDGED;
842                    break;
843                case CANCEL:
844                    if (getDocumentService().documentExists(document.getDocumentNumber())) {
845                        document = getDocumentService().cancelDocument(document, form.getAnnotation());
846                        successMessageKey = RiceKeyConstants.MESSAGE_CANCELLED;
847                    }
848                    break;
849                case COMPLETE:
850                    if (getDocumentService().documentExists(document.getDocumentNumber())) {
851                        document = getDocumentService().completeDocument(document, form.getAnnotation(),
852                                combineAdHocRecipients(form));
853                        successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_SUCCESSFUL;
854                    }
855                    break;
856                case SENDADHOCREQUESTS:
857                    getDocumentService().sendAdHocRequests(document, form.getAnnotation(),  combineAdHocRecipients(form));
858                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_SUCCESSFUL;
859                    break;
860                case RECALL:
861                    if (getDocumentService().documentExists(document.getDocumentNumber())) {
862                        String recallExplanation = form.getDialogExplanations().get(
863                                KRADConstants.QUESTION_ACTION_RECALL_REASON);
864                        document = getDocumentService().recallDocument(document, recallExplanation, true);
865                        successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_RECALLED;
866                    }
867                    break;
868            }
869
870            // push potentially updated document back into the form
871            form.setDocument(document);
872
873            if (successMessageKey != null) {
874                GlobalVariables.getMessageMap().putInfo(KRADConstants.GLOBAL_MESSAGES, successMessageKey);
875            }
876        } catch (ValidationException e) {
877            // log the error and swallow exception so screen will draw with errors.
878            // we don't want the exception to bubble up and the user to see an incident page, but instead just return to
879            // the page and display the actual errors. This would need a fix to the API at some point.
880            KRADUtils.logErrors();
881            LOG.error("Validation Exception occured for document :" + document.getDocumentNumber(), e);
882
883            // if no errors in map then throw runtime because something bad happened
884            if (GlobalVariables.getMessageMap().hasNoErrors()) {
885                throw new RiceRuntimeException("Validation Exception with no error message.", e);
886            }
887        } catch (Exception e) {
888            throw new RiceRuntimeException(
889                    "Exception trying to invoke action " + action.name() + " for document: " + document
890                            .getDocumentNumber(), e);
891        }
892
893        form.setAnnotation("");
894    }
895
896    /**
897     * Convenience method to combine the two lists of ad hoc recipients into one which should be done before
898     * calling any of the document service methods that expect a list of ad hoc recipients.
899     *
900     * @param form document form instance containing the ad hod lists
901     * @return List<AdHocRouteRecipient> combined ad hoc recipients
902     */
903    protected List<AdHocRouteRecipient> combineAdHocRecipients(DocumentFormBase form) {
904        Document document = form.getDocument();
905
906        List<AdHocRouteRecipient> adHocRecipients = new ArrayList<AdHocRouteRecipient>();
907        adHocRecipients.addAll(document.getAdHocRoutePersons());
908        adHocRecipients.addAll(document.getAdHocRouteWorkgroups());
909
910        return adHocRecipients;
911    }
912
913    /**
914     * {@inheritDoc}
915     */
916    @Override
917    public void performSuperUserWorkflowAction(DocumentFormBase form, UifConstants.SuperUserWorkflowAction action) {
918        performSuperUserWorkflowAction(form, action, null);
919    }
920
921    /**
922     * {@inheritDoc}
923     */
924    @Override
925    public void performSuperUserWorkflowAction(DocumentFormBase form, UifConstants.SuperUserWorkflowAction action,
926            ActionRequest actionRequest) {
927        Document document = form.getDocument();
928
929        if (LOG.isDebugEnabled()) {
930            LOG.debug("Performing super user workflow action " + action.name() + "for document: " + document.getDocumentNumber());
931        }
932
933        try {
934            String documentTypeId = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeId();
935            String documentNumber = document.getDocumentNumber();
936            String principalId = GlobalVariables.getUserSession().getPrincipalId();
937            String superUserAnnotation = document.getSuperUserAnnotation();
938
939            WorkflowDocumentActionsService documentActions = getWorkflowDocumentActionsService(documentTypeId);
940            DocumentActionParameters parameters = DocumentActionParameters.create(documentNumber, principalId, superUserAnnotation);
941
942            String successMessageKey = null;
943            switch (action) {
944                case TAKEACTION:
945                    if (actionRequest != null) {
946                        documentActions.superUserTakeRequestedAction(parameters, true, actionRequest.getId());
947
948                        String actionRequestedCode = actionRequest.getActionRequested().getCode();
949                        if (StringUtils.equals(actionRequestedCode, ActionRequestType.ACKNOWLEDGE.getCode())) {
950                            successMessageKey = RiceKeyConstants.MESSAGE_SUPER_USER_ACTION_REQUEST_ACKNOWLEDGED;
951                        } else if (StringUtils.equals(actionRequestedCode, ActionRequestType.FYI.getCode())) {
952                            successMessageKey = RiceKeyConstants.MESSAGE_SUPER_USER_ACTION_REQUEST_FYIED;
953                        } else if (StringUtils.equals(actionRequestedCode, ActionRequestType.COMPLETE.getCode())) {
954                            successMessageKey = RiceKeyConstants.MESSAGE_SUPER_USER_ACTION_REQUEST_COMPLETED;
955                        } else if (StringUtils.equals(actionRequestedCode, ActionRequestType.APPROVE.getCode())) {
956                            successMessageKey = RiceKeyConstants.MESSAGE_SUPER_USER_ACTION_REQUEST_APPROVED;
957                        } else {
958                            successMessageKey = RiceKeyConstants.MESSAGE_SUPER_USER_ACTION_REQUEST_APPROVED;
959                        }
960                    }
961                    break;
962                case APPROVE:
963                    documentActions.superUserBlanketApprove(parameters, true);
964
965                    successMessageKey = RiceKeyConstants.MESSAGE_SUPER_USER_APPROVED;
966                    break;
967                case DISAPPROVE:
968                    documentActions.superUserDisapprove(parameters, true);
969
970                    successMessageKey = RiceKeyConstants.MESSAGE_SUPER_USER_DISAPPROVED;
971                    break;
972            }
973
974            form.setEvaluateFlagsAndModes(true);
975            form.setCanEditView(null);
976
977            if (successMessageKey != null) {
978                if (actionRequest != null) {
979                    GlobalVariables.getMessageMap().putInfo(KRADConstants.GLOBAL_MESSAGES, successMessageKey,
980                            document.getDocumentNumber(), actionRequest.getId());
981                } else {
982                    GlobalVariables.getMessageMap().putInfo(KRADConstants.GLOBAL_MESSAGES, successMessageKey,
983                            document.getDocumentNumber());
984                }
985            }
986        } catch (ValidationException e) {
987            // log the error and swallow exception so screen will draw with errors.
988            // we don't want the exception to bubble up and the user to see an incident page, but instead just return to
989            // the page and display the actual errors. This would need a fix to the API at some point.
990            KRADUtils.logErrors();
991            LOG.error("Validation Exception occured for document :" + document.getDocumentNumber(), e);
992
993            // if no errors in map then throw runtime because something bad happened
994            if (GlobalVariables.getMessageMap().hasNoErrors()) {
995                throw new RiceRuntimeException("Validation Exception with no error message.", e);
996            }
997        } catch (Exception e) {
998            throw new RiceRuntimeException(
999                    "Exception trying to invoke action " + action.name() + " for document: " + document
1000                            .getDocumentNumber(), e);
1001        }
1002
1003        document.setSuperUserAnnotation("");
1004    }
1005
1006    /**
1007     * Helper method to get the correct {@link WorkflowDocumentActionsService} from the {@code applicationId} of the
1008     * document type.
1009     *
1010     * @param documentTypeId the document type to get the application id from
1011     *
1012     * @return the correct {@link WorkflowDocumentActionsService} from the {@code applicationId} of the document type
1013     */
1014    protected WorkflowDocumentActionsService getWorkflowDocumentActionsService(String documentTypeId) {
1015        DocumentType documentType = KewApiServiceLocator.getDocumentTypeService().getDocumentTypeById(documentTypeId);
1016        String applicationId = documentType.getApplicationId();
1017        QName serviceName = new QName(KewApiConstants.Namespaces.KEW_NAMESPACE_2_0,
1018                KewApiConstants.ServiceNames.WORKFLOW_DOCUMENT_ACTIONS_SERVICE_SOAP);
1019
1020        WorkflowDocumentActionsService service = (WorkflowDocumentActionsService) KsbApiServiceLocator.getServiceBus()
1021                .getService(serviceName, applicationId);
1022
1023        if (service == null) {
1024            service = KewApiServiceLocator.getWorkflowDocumentActionsService();
1025        }
1026
1027        return service;
1028    }
1029
1030    /**
1031     * Helper method to check if sensitive data is present in a given string and dialog display.
1032     *
1033     * <p>If the string is sensitive we want to return a dialog box to make sure user wants to continue,
1034     * else we just return null</p>
1035     *
1036     * @param field the string to check for sensitive data
1037     * @param form the form to add the dialog to
1038     * @return the model and view for the dialog or null if there isn't one
1039     */
1040    protected ModelAndView checkSensitiveDataAndWarningDialog(String field, UifFormBase form) {
1041        boolean hasSensitiveData = KRADUtils.containsSensitiveDataPatternMatch(field);
1042        Boolean warnForSensitiveData = getParameterService().getParameterValueAsBoolean(KRADConstants.KNS_NAMESPACE,
1043                ParameterConstants.ALL_COMPONENT,
1044                KRADConstants.SystemGroupParameterNames.SENSITIVE_DATA_PATTERNS_WARNING_IND);
1045
1046        // if there is sensitive data and the flag to warn for sensitive data is set,
1047        // then we want a dialog returned if there is not already one
1048        if (hasSensitiveData && warnForSensitiveData.booleanValue()) {
1049            DialogResponse sensitiveDataDialogResponse = form.getDialogResponse(SENSITIVE_DATA_DIALOG);
1050
1051            if (sensitiveDataDialogResponse == null) {
1052                // no sensitive data dialog found, so create one on the form and return it
1053                return getModelAndViewService().showDialog(SENSITIVE_DATA_DIALOG, true, form);
1054            }
1055        }
1056
1057        return null;
1058    }
1059
1060    /**
1061     * Convenience method for building document authorization exceptions.
1062     *
1063     * @param action the action that was requested
1064     * @param document document instance the action was requested for
1065     */
1066    protected DocumentAuthorizationException buildAuthorizationException(String action, Document document) {
1067        return new DocumentAuthorizationException(GlobalVariables.getUserSession().getPerson().getPrincipalName(),
1068                action, document.getDocumentNumber());
1069    }
1070
1071    protected LegacyDataAdapter getLegacyDataAdapter() {
1072        if (this.legacyDataAdapter == null) {
1073            this.legacyDataAdapter = KRADServiceLocatorWeb.getLegacyDataAdapter();
1074        }
1075        return this.legacyDataAdapter;
1076    }
1077
1078    public void setLegacyDataAdapter(LegacyDataAdapter legacyDataAdapter) {
1079        this.legacyDataAdapter = legacyDataAdapter;
1080    }
1081
1082    protected DataDictionaryService getDataDictionaryService() {
1083        if (this.dataDictionaryService == null) {
1084            this.dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
1085        }
1086        return this.dataDictionaryService;
1087    }
1088
1089    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1090        this.dataDictionaryService = dataDictionaryService;
1091    }
1092
1093    protected DocumentService getDocumentService() {
1094        if (this.documentService == null) {
1095            this.documentService = KRADServiceLocatorWeb.getDocumentService();
1096        }
1097        return this.documentService;
1098    }
1099
1100    public void setDocumentService(DocumentService documentService) {
1101        this.documentService = documentService;
1102    }
1103
1104    protected DocumentDictionaryService getDocumentDictionaryService() {
1105        if (this.documentDictionaryService == null) {
1106            this.documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1107        }
1108        return this.documentDictionaryService;
1109    }
1110
1111    public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1112        this.documentDictionaryService = documentDictionaryService;
1113    }
1114
1115    protected AttachmentService getAttachmentService() {
1116        if (attachmentService == null) {
1117            attachmentService = KRADServiceLocator.getAttachmentService();
1118        }
1119        return this.attachmentService;
1120    }
1121
1122    public void setAttachmentService(AttachmentService attachmentService) {
1123        this.attachmentService = attachmentService;
1124    }
1125
1126    protected NoteService getNoteService() {
1127        if (noteService == null) {
1128            noteService = KRADServiceLocator.getNoteService();
1129        }
1130
1131        return this.noteService;
1132    }
1133
1134    public void setNoteService(NoteService noteService) {
1135        this.noteService = noteService;
1136    }
1137
1138    protected ModelAndViewService getModelAndViewService() {
1139        return modelAndViewService;
1140    }
1141
1142    public void setModelAndViewService(ModelAndViewService modelAndViewService) {
1143        this.modelAndViewService = modelAndViewService;
1144    }
1145
1146    protected NavigationControllerService getNavigationControllerService() {
1147        return navigationControllerService;
1148    }
1149
1150    public void setNavigationControllerService(NavigationControllerService navigationControllerService) {
1151        this.navigationControllerService = navigationControllerService;
1152    }
1153
1154    protected ConfigurationService getConfigurationService() {
1155        return configurationService;
1156    }
1157
1158    public void setConfigurationService(ConfigurationService configurationService) {
1159        this.configurationService = configurationService;
1160    }
1161
1162    protected CollectionControllerService getCollectionControllerService() {
1163        return collectionControllerService;
1164    }
1165
1166    public void setCollectionControllerService(CollectionControllerService collectionControllerService) {
1167        this.collectionControllerService = collectionControllerService;
1168    }
1169
1170    protected ParameterService getParameterService() {
1171        return parameterService;
1172    }
1173
1174    public void setParameterService(ParameterService parameterService) {
1175        this.parameterService = parameterService;
1176    }
1177}