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.web.controller;
017
018import org.apache.commons.lang.ArrayUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.config.property.ConfigurationService;
021import org.kuali.rice.core.api.exception.RiceRuntimeException;
022import org.kuali.rice.core.api.util.RiceKeyConstants;
023import org.kuali.rice.kew.api.KewApiConstants;
024import org.kuali.rice.kew.api.WorkflowDocument;
025import org.kuali.rice.kew.api.exception.WorkflowException;
026import org.kuali.rice.kim.api.identity.Person;
027import org.kuali.rice.krad.UserSessionUtils;
028import org.kuali.rice.krad.bo.AdHocRouteRecipient;
029import org.kuali.rice.krad.bo.Attachment;
030import org.kuali.rice.krad.bo.DocumentHeader;
031import org.kuali.rice.krad.bo.Note;
032import org.kuali.rice.krad.document.Document;
033import org.kuali.rice.krad.document.DocumentAuthorizer;
034import org.kuali.rice.krad.maintenance.MaintenanceDocument;
035import org.kuali.rice.krad.exception.DocumentAuthorizationException;
036import org.kuali.rice.krad.exception.UnknownDocumentIdException;
037import org.kuali.rice.krad.exception.ValidationException;
038import org.kuali.rice.krad.rules.rule.event.AddNoteEvent;
039import org.kuali.rice.krad.service.AttachmentService;
040import org.kuali.rice.krad.service.BusinessObjectService;
041import org.kuali.rice.krad.service.DataDictionaryService;
042import org.kuali.rice.krad.service.DocumentDictionaryService;
043import org.kuali.rice.krad.service.DocumentService;
044import org.kuali.rice.krad.service.KRADServiceLocator;
045import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
046import org.kuali.rice.krad.service.NoteService;
047import org.kuali.rice.krad.uif.UifConstants.WorkflowAction;
048import org.kuali.rice.krad.uif.UifParameters;
049import org.kuali.rice.krad.uif.UifPropertyPaths;
050import org.kuali.rice.krad.uif.container.CollectionGroup;
051import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
052import org.kuali.rice.krad.util.GlobalVariables;
053import org.kuali.rice.krad.util.KRADConstants;
054import org.kuali.rice.krad.util.NoteType;
055import org.kuali.rice.krad.web.form.DocumentFormBase;
056import org.kuali.rice.krad.web.form.UifFormBase;
057import org.springframework.util.FileCopyUtils;
058import org.springframework.validation.BindingResult;
059import org.springframework.web.bind.ServletRequestBindingException;
060import org.springframework.web.bind.annotation.ModelAttribute;
061import org.springframework.web.bind.annotation.RequestMapping;
062import org.springframework.web.bind.annotation.RequestMethod;
063import org.springframework.web.multipart.MultipartFile;
064import org.springframework.web.servlet.ModelAndView;
065
066import javax.servlet.http.HttpServletRequest;
067import javax.servlet.http.HttpServletResponse;
068import java.io.FileNotFoundException;
069import java.io.IOException;
070import java.io.InputStream;
071import java.util.ArrayList;
072import java.util.List;
073import java.util.Properties;
074
075/**
076 * Base controller class for all KRAD <code>DocumentView</code> screens working
077 * with <code>Document</code> models
078 *
079 * <p>
080 * Provides default controller implementations for the standard document actions including: doc handler
081 * (retrieve from doc search and action list), save, route (and other KEW actions)
082 * </p>
083 *
084 * @author Kuali Rice Team (rice.collab@kuali.org)
085 */
086public abstract class DocumentControllerBase extends UifControllerBase {
087    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentControllerBase.class);
088
089    // COMMAND constants which cause docHandler to load an existing document
090    // instead of creating a new one
091    protected static final String[] DOCUMENT_LOAD_COMMANDS =
092            {KewApiConstants.ACTIONLIST_COMMAND, KewApiConstants.DOCSEARCH_COMMAND, KewApiConstants.SUPERUSER_COMMAND,
093                    KewApiConstants.HELPDESK_ACTIONLIST_COMMAND};
094
095    private BusinessObjectService businessObjectService;
096    private DataDictionaryService dataDictionaryService;
097    private DocumentService documentService;
098    private DocumentDictionaryService documentDictionaryService;
099    private AttachmentService attachmentService;
100    private NoteService noteService;
101
102    /**
103     * @see org.kuali.rice.krad.web.controller.UifControllerBase#createInitialForm(javax.servlet.http.HttpServletRequest)
104     */
105    @Override
106    protected abstract DocumentFormBase createInitialForm(HttpServletRequest request);
107
108    /**
109     * Used to funnel all document handling through, we could do useful things
110     * like log and record various openings and status Additionally it may be
111     * nice to have a single dispatcher that can know how to dispatch to a
112     * redirect url for document specific handling but we may not need that as
113     * all we should need is the document to be able to load itself based on
114     * document id and then which action forward or redirect is pertinent for
115     * the document type.
116     */
117    @RequestMapping(params = "methodToCall=docHandler")
118    public ModelAndView docHandler(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
119            HttpServletRequest request, HttpServletResponse response) throws Exception {
120        String command = form.getCommand();
121
122        // in all of the following cases we want to load the document
123        if (ArrayUtils.contains(DOCUMENT_LOAD_COMMANDS, command) && form.getDocId() != null) {
124            loadDocument(form);
125        } else if (KewApiConstants.INITIATE_COMMAND.equals(command)) {
126            createDocument(form);
127        } else {
128            LOG.error("docHandler called with invalid parameters");
129            throw new IllegalArgumentException("docHandler called with invalid parameters");
130        }
131
132        // TODO: authorization on document actions
133        // if (KEWConstants.SUPERUSER_COMMAND.equalsIgnoreCase(command)) {
134        // form.setSuppressAllButtons(true);
135        // }
136
137        return getUIFModelAndView(form);
138    }
139
140    /**
141     * Loads the document by its provided document header id. This has been abstracted out so that
142     * it can be overridden in children if the need arises
143     *
144     * @param form - form instance that contains the doc id parameter and where
145     * the retrieved document instance should be set
146     */
147    protected void loadDocument(DocumentFormBase form) throws WorkflowException {
148        String docId = form.getDocId();
149
150        LOG.debug("Loading document" + docId);
151
152        Document doc = null;
153        doc = getDocumentService().getByDocumentHeaderId(docId);
154        if (doc == null) {
155            throw new UnknownDocumentIdException(
156                    "Document no longer exists.  It may have been cancelled before being saved.");
157        }
158
159        WorkflowDocument workflowDocument = doc.getDocumentHeader().getWorkflowDocument();
160        if (!getDocumentDictionaryService().getDocumentAuthorizer(doc).canOpen(doc,
161                GlobalVariables.getUserSession().getPerson())) {
162            throw buildAuthorizationException("open", doc);
163        }
164
165        // re-retrieve the document using the current user's session - remove
166        // the system user from the WorkflowDcument object
167        if (workflowDocument != doc.getDocumentHeader().getWorkflowDocument()) {
168            LOG.warn("Workflow document changed via canOpen check");
169            doc.getDocumentHeader().setWorkflowDocument(workflowDocument);
170        }
171
172        form.setDocument(doc);
173        WorkflowDocument workflowDoc = doc.getDocumentHeader().getWorkflowDocument();
174        form.setDocTypeName(workflowDoc.getDocumentTypeName());
175
176        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(), workflowDoc);
177    }
178
179    /**
180     * Creates a new document of the type specified by the docTypeName property of the given form.
181     * This has been abstracted out so that it can be overridden in children if the need arises.
182     *
183     * @param form - form instance that contains the doc type parameter and where
184     * the new document instance should be set
185     */
186    protected void createDocument(DocumentFormBase form) throws WorkflowException {
187        LOG.debug("Creating new document instance for doc type: " + form.getDocTypeName());
188        Document doc = getDocumentService().getNewDocument(form.getDocTypeName());
189
190        form.setDocument(doc);
191        form.setDocTypeName(doc.getDocumentHeader().getWorkflowDocument().getDocumentTypeName());
192    }
193
194    /**
195     * Reloads the document contained on the form from the database
196     *
197     * @param form - document form base containing the document instance from which the document number will
198     * be retrieved and used to fetch the document from the database
199     * @return ModelAndView
200     */
201    @RequestMapping(params = "methodToCall=reload")
202    public ModelAndView reload(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
203            HttpServletRequest request, HttpServletResponse response) throws Exception {
204        Document document = form.getDocument();
205
206        // prepare for the reload action - set doc id and command
207        form.setDocId(document.getDocumentNumber());
208        form.setCommand(DOCUMENT_LOAD_COMMANDS[1]);
209
210        GlobalVariables.getMessageMap().putInfo(KRADConstants.GLOBAL_MESSAGES, RiceKeyConstants.MESSAGE_RELOADED);
211
212        // forward off to the doc handler
213        return docHandler(form, result, request, response);
214    }
215
216    /**
217     * Prompts user to confirm the cancel action then if confirmed cancels the document instance
218     * contained on the form
219     *
220     * @param form - document form base containing the document instance that will be cancelled
221     * @return ModelAndView
222     */
223    @RequestMapping(params = "methodToCall=cancel")
224    @Override
225    public ModelAndView cancel(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
226            HttpServletRequest request, HttpServletResponse response) {
227        DocumentFormBase documentForm = (DocumentFormBase) form;
228
229        // TODO: prompt user to confirm the cancel, need question framework
230
231        performWorkflowAction(documentForm, WorkflowAction.CANCEL, false);
232
233        return returnToPrevious(form);
234    }
235
236    /**
237     * Saves the document instance contained on the form
238     *
239     * @param form - document form base containing the document instance that will be saved
240     * @return ModelAndView
241     */
242    @RequestMapping(params = "methodToCall=save")
243    public ModelAndView save(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
244            HttpServletRequest request, HttpServletResponse response) throws Exception {
245        performWorkflowAction(form, WorkflowAction.SAVE, true);
246
247        return getUIFModelAndView(form);
248    }
249
250    /**
251     * Completes the document instance contained on the form
252     *
253     * @param form - document form base containing the document instance that will be completed
254     * @return ModelAndView
255     */
256    @RequestMapping(params = "methodToCall=complete")
257    public ModelAndView complete(@ModelAttribute("KualiForm")
258    DocumentFormBase form, BindingResult result, HttpServletRequest request, HttpServletResponse response) throws Exception {
259        performWorkflowAction(form, WorkflowAction.COMPLETE, true);
260
261        return getUIFModelAndView(form);
262    }
263
264    /**
265     * Routes the document instance contained on the form
266     *
267     * @param form - document form base containing the document instance that will be routed
268     * @return ModelAndView
269     */
270    @RequestMapping(params = "methodToCall=route")
271    public ModelAndView route(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
272            HttpServletRequest request, HttpServletResponse response) {
273        performWorkflowAction(form, WorkflowAction.ROUTE, true);
274
275        return getUIFModelAndView(form);
276    }
277
278    /**
279     * Performs the blanket approve workflow action on the form document instance
280     *
281     * @param form - document form base containing the document instance that will be blanket approved
282     * @return ModelAndView
283     */
284    @RequestMapping(params = "methodToCall=blanketApprove")
285    public ModelAndView blanketApprove(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
286            HttpServletRequest request, HttpServletResponse response) throws Exception {
287        performWorkflowAction(form, WorkflowAction.BLANKETAPPROVE, true);
288
289        return returnToPrevious(form);
290    }
291
292    /**
293     * Performs the approve workflow action on the form document instance
294     *
295     * @param form - document form base containing the document instance that will be approved
296     * @return ModelAndView
297     */
298    @RequestMapping(params = "methodToCall=approve")
299    public ModelAndView approve(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
300            HttpServletRequest request, HttpServletResponse response) throws Exception {
301        performWorkflowAction(form, WorkflowAction.APPROVE, true);
302
303        return returnToPrevious(form);
304    }
305
306    /**
307     * Performs the disapprove workflow action on the form document instance
308     *
309     * @param form - document form base containing the document instance that will be disapproved
310     * @return ModelAndView
311     */
312    @RequestMapping(params = "methodToCall=disapprove")
313    public ModelAndView disapprove(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
314            HttpServletRequest request, HttpServletResponse response) throws Exception {
315        // TODO: need to prompt for disapproval note text
316        performWorkflowAction(form, WorkflowAction.DISAPPROVE, true);
317
318        return returnToPrevious(form);
319    }
320
321    /**
322     * Performs the fyi workflow action on the form document instance
323     *
324     * @param form - document form base containing the document instance the fyi will be taken on
325     * @return ModelAndView
326     */
327    @RequestMapping(params = "methodToCall=fyi")
328    public ModelAndView fyi(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
329            HttpServletRequest request, HttpServletResponse response) throws Exception {
330        performWorkflowAction(form, WorkflowAction.FYI, false);
331
332        return returnToPrevious(form);
333    }
334
335    /**
336     * Performs the acknowledge workflow action on the form document instance
337     *
338     * @param form - document form base containing the document instance the acknowledge will be taken on
339     * @return ModelAndView
340     */
341    @RequestMapping(params = "methodToCall=acknowledge")
342    public ModelAndView acknowledge(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
343            HttpServletRequest request, HttpServletResponse response) throws Exception {
344        performWorkflowAction(form, WorkflowAction.ACKNOWLEDGE, false);
345
346        return returnToPrevious(form);
347    }
348
349    /**
350     * Invokes the {@link DocumentService} to carry out a request workflow action and adds a success message, if
351     * requested a check for sensitive data is also performed
352     *
353     * @param form - document form instance containing the document for which the action will be taken on
354     * @param action - {@link WorkflowAction} enum indicating what workflow action to take
355     * @param checkSensitiveData - boolean indicating whether a check for sensitive data should occur
356     */
357    protected void performWorkflowAction(DocumentFormBase form, WorkflowAction action, boolean checkSensitiveData) {
358        Document document = form.getDocument();
359
360        LOG.debug("Performing workflow action " + action.name() + "for document: " + document.getDocumentNumber());
361
362        // TODO: need question and prompt framework
363        if (checkSensitiveData) {
364            //        String viewName = checkAndWarnAboutSensitiveData(form, request, response,
365            //                KRADPropertyConstants.DOCUMENT_EXPLANATION, document.getDocumentHeader().getExplanation(), "route", "");
366            //        if (viewName != null) {
367            //            return new ModelAndView(viewName);
368            //        }
369        }
370
371        try {
372            String successMessageKey = null;
373            switch (action) {
374                case SAVE:
375                    getDocumentService().saveDocument(document);
376                    successMessageKey = RiceKeyConstants.MESSAGE_SAVED;
377                    break;
378                case ROUTE:
379                    getDocumentService().routeDocument(document, form.getAnnotation(), combineAdHocRecipients(form));
380                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_SUCCESSFUL;
381                    break;
382                case BLANKETAPPROVE:
383                    getDocumentService().blanketApproveDocument(document, form.getAnnotation(), combineAdHocRecipients(
384                            form));
385                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_APPROVED;
386                    break;
387                case APPROVE:
388                    getDocumentService().approveDocument(document, form.getAnnotation(), combineAdHocRecipients(form));
389                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_APPROVED;
390                    break;
391                case DISAPPROVE:
392                    // TODO: need to get disapprove note from user
393                    String disapprovalNoteText = "";
394                    getDocumentService().disapproveDocument(document, disapprovalNoteText);
395                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_DISAPPROVED;
396                    break;
397                case FYI:
398                    getDocumentService().clearDocumentFyi(document, combineAdHocRecipients(form));
399                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_FYIED;
400                    break;
401                case ACKNOWLEDGE:
402                    getDocumentService().acknowledgeDocument(document, form.getAnnotation(), combineAdHocRecipients(
403                            form));
404                    successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_ACKNOWLEDGED;
405                    break;
406                case CANCEL:
407                    if (getDocumentService().documentExists(document.getDocumentNumber())) {
408                        getDocumentService().cancelDocument(document, form.getAnnotation());
409                        successMessageKey = RiceKeyConstants.MESSAGE_CANCELLED;
410                    }
411                    break;
412                case COMPLETE:
413                    if (getDocumentService().documentExists(document.getDocumentNumber())) {
414                        getDocumentService().completeDocument(document, form.getAnnotation(), combineAdHocRecipients(form));
415                        successMessageKey = RiceKeyConstants.MESSAGE_ROUTE_SUCCESSFUL;
416                    }
417                    break;
418            }
419
420            if (successMessageKey != null) {
421                GlobalVariables.getMessageMap().putInfo(KRADConstants.GLOBAL_MESSAGES, successMessageKey);
422            }
423        } catch (ValidationException e) {
424            // if errors in map, swallow exception so screen will draw with errors
425            // if not then throw runtime because something bad happened
426            if (GlobalVariables.getMessageMap().hasNoErrors()) {
427                throw new RiceRuntimeException("Validation Exception with no error message.", e);
428            }
429        } catch (Exception e) {
430            throw new RiceRuntimeException(
431                    "Exception trying to invoke action " + action.name() + "for document: " + document
432                            .getDocumentNumber(), e);
433        }
434
435        form.setAnnotation("");
436    }
437
438    /**
439     * Redirects to the supervisor functions page
440     *
441     * @return ModelAndView - model and view configured for the redirect URL
442     */
443    @RequestMapping(params = "methodToCall=supervisorFunctions")
444    public ModelAndView supervisorFunctions(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
445            HttpServletRequest request, HttpServletResponse response) throws Exception {
446
447        String workflowSuperUserUrl = getConfigurationService().getPropertyValueAsString(KRADConstants.WORKFLOW_URL_KEY)
448                + "/" + KRADConstants.SUPERUSER_ACTION;
449
450        Properties props = new Properties();
451        props.put(UifParameters.METHOD_TO_CALL, "displaySuperUserDocument");
452        props.put(UifPropertyPaths.DOCUMENT_ID, form.getDocument().getDocumentNumber());
453
454        return performRedirect(form, workflowSuperUserUrl, props);
455    }
456
457    /**
458     * Called by the add note action for adding a note. Method validates, saves attachment and adds the
459     * time stamp and author. Calls the UifControllerBase.addLine method to handle generic actions.
460     *
461     * @param uifForm - document form base containing the note instance that will be inserted into the document
462     * @return ModelAndView
463     */
464    @RequestMapping(method = RequestMethod.POST, params = "methodToCall=insertNote")
465    public ModelAndView insertNote(@ModelAttribute("KualiForm") UifFormBase uifForm, BindingResult result,
466            HttpServletRequest request, HttpServletResponse response) {
467
468        // Get the note add line
469        String selectedCollectionPath = uifForm.getActionParamaterValue(UifParameters.SELLECTED_COLLECTION_PATH);
470        CollectionGroup collectionGroup = uifForm.getPostedView().getViewIndex().getCollectionGroupByPath(
471                selectedCollectionPath);
472        String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
473        Object addLine = ObjectPropertyUtils.getPropertyValue(uifForm, addLinePath);
474        Note newNote = (Note) addLine;
475        newNote.setNotePostedTimestampToCurrent();
476
477        Document document = ((DocumentFormBase) uifForm).getDocument();
478
479        newNote.setRemoteObjectIdentifier(document.getNoteTarget().getObjectId());
480
481        // Get the attachment file
482        String attachmentTypeCode = null;
483        MultipartFile attachmentFile = uifForm.getAttachmentFile();
484        Attachment attachment = null;
485        if (attachmentFile != null && !StringUtils.isBlank(attachmentFile.getOriginalFilename())) {
486            if (attachmentFile.getSize() == 0) {
487                GlobalVariables.getMessageMap().putError(String.format("%s.%s",
488                        KRADConstants.NEW_DOCUMENT_NOTE_PROPERTY_NAME,
489                        KRADConstants.NOTE_ATTACHMENT_FILE_PROPERTY_NAME), RiceKeyConstants.ERROR_UPLOADFILE_EMPTY,
490                        attachmentFile.getOriginalFilename());
491            } else {
492                if (newNote.getAttachment() != null) {
493                    attachmentTypeCode = newNote.getAttachment().getAttachmentTypeCode();
494                }
495
496                DocumentAuthorizer documentAuthorizer = getDocumentDictionaryService().getDocumentAuthorizer(document);
497                if (!documentAuthorizer.canAddNoteAttachment(document, attachmentTypeCode,
498                        GlobalVariables.getUserSession().getPerson())) {
499                    throw buildAuthorizationException("annotate", document);
500                }
501
502                try {
503                    String attachmentType = null;
504                    Attachment newAttachment = newNote.getAttachment();
505                    if (newAttachment != null) {
506                        attachmentType = newAttachment.getAttachmentTypeCode();
507                    }
508
509                    attachment = getAttachmentService().createAttachment(document.getNoteTarget(),
510                            attachmentFile.getOriginalFilename(), attachmentFile.getContentType(),
511                            (int) attachmentFile.getSize(), attachmentFile.getInputStream(), attachmentType);
512                } catch (IOException e) {
513                    throw new RiceRuntimeException("Unable to store attachment", e);
514                }
515            }
516        }
517
518        Person kualiUser = GlobalVariables.getUserSession().getPerson();
519        if (kualiUser == null) {
520            throw new IllegalStateException("Current UserSession has a null Person.");
521        }
522
523        newNote.setAuthorUniversalIdentifier(kualiUser.getPrincipalId());
524
525        // validate the note
526        boolean rulePassed = KRADServiceLocatorWeb.getKualiRuleService().applyRules(new AddNoteEvent(document,
527                newNote));
528
529        // if the rule evaluation passed, let's add the note
530        if (rulePassed) {
531            newNote.refresh();
532
533            DocumentHeader documentHeader = document.getDocumentHeader();
534
535            // adding the attachment after refresh gets called, since the attachment record doesn't get persisted
536            // until the note does (and therefore refresh doesn't have any attachment to autoload based on the id, nor does it
537            // autopopulate the id since the note hasn't been persisted yet)
538            if (attachment != null) {
539                newNote.addAttachment(attachment);
540            }
541            // Save the note if the document is already saved
542            if (!documentHeader.getWorkflowDocument().isInitiated() && StringUtils.isNotEmpty(
543                    document.getNoteTarget().getObjectId()) && !(document instanceof MaintenanceDocument && NoteType
544                    .BUSINESS_OBJECT.getCode().equals(newNote.getNoteTypeCode()))) {
545
546                getNoteService().save(newNote);
547            }
548
549        }
550
551        return addLine(uifForm, result, request, response);
552    }
553
554    /**
555     * Called by the delete note action for deleting a note.
556     * Calls the UifControllerBase.deleteLine method to handle
557     * generic actions.
558     */
559    @RequestMapping(method = RequestMethod.POST, params = "methodToCall=deleteNote")
560    public ModelAndView deleteNote(@ModelAttribute("KualiForm") UifFormBase uifForm, BindingResult result,
561            HttpServletRequest request, HttpServletResponse response) {
562
563        String selectedLineIndex = uifForm.getActionParamaterValue("selectedLineIndex");
564        Document document = ((DocumentFormBase) uifForm).getDocument();
565        Note note = document.getNote(Integer.parseInt(selectedLineIndex));
566
567        Attachment attachment = note.getAttachment();
568        String attachmentTypeCode = null;
569        if (attachment != null) {
570            attachmentTypeCode = attachment.getAttachmentTypeCode();
571        }
572
573        // check delete note authorization
574        Person user = GlobalVariables.getUserSession().getPerson();
575        String authorUniversalIdentifier = note.getAuthorUniversalIdentifier();
576        if (!getDocumentDictionaryService().getDocumentAuthorizer(document).canDeleteNoteAttachment(document,
577                attachmentTypeCode, authorUniversalIdentifier, user)) {
578            throw buildAuthorizationException("annotate", document);
579        }
580
581        if (attachment != null && attachment.isComplete()) { // only do this if the note has been persisted
582            //KFSMI-798 - refresh() changed to refreshNonUpdateableReferences()
583            //All references for the business object Attachment are auto-update="none",
584            //so refreshNonUpdateableReferences() should work the same as refresh()
585            if (note.getNoteIdentifier()
586                    != null) { // KULRICE-2343 don't blow away note reference if the note wasn't persisted
587                attachment.refreshNonUpdateableReferences();
588            }
589            getAttachmentService().deleteAttachmentContents(attachment);
590        }
591        // delete the note if the document is already saved
592        if (!document.getDocumentHeader().getWorkflowDocument().isInitiated()) {
593            getNoteService().deleteNote(note);
594        }
595
596        return deleteLine(uifForm, result, request, response);
597    }
598
599    /**
600     * Called by the download attachment action on a note. Method
601     * gets the attachment input stream from the AttachmentService
602     * and writes it to the request output stream.
603     */
604    @RequestMapping(method = RequestMethod.POST, params = "methodToCall=downloadAttachment")
605    public ModelAndView downloadAttachment(@ModelAttribute("KualiForm") UifFormBase uifForm, BindingResult result,
606            HttpServletRequest request,
607            HttpServletResponse response) throws ServletRequestBindingException, FileNotFoundException, IOException {
608        // Get the attachment input stream
609        String selectedLineIndex = uifForm.getActionParamaterValue("selectedLineIndex");
610        Note note = ((DocumentFormBase) uifForm).getDocument().getNote(Integer.parseInt(selectedLineIndex));
611        Attachment attachment = note.getAttachment();
612        InputStream is = getAttachmentService().retrieveAttachmentContents(attachment);
613
614        // Set the response headers
615        response.setContentType(attachment.getAttachmentMimeTypeCode());
616        response.setContentLength(attachment.getAttachmentFileSize().intValue());
617        response.setHeader("Expires", "0");
618        response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
619        response.setHeader("Pragma", "public");
620        response.setHeader("Content-Disposition",
621                "attachment; filename=\"" + attachment.getAttachmentFileName() + "\"");
622
623        // Copy the input stream to the response
624        FileCopyUtils.copy(is, response.getOutputStream());
625        return null;
626    }
627
628    /**
629     * Called by the cancel attachment action on a note. Method
630     * removes the attachment file from the form.
631     */
632    @RequestMapping(method = RequestMethod.POST, params = "methodToCall=cancelAttachment")
633    public ModelAndView cancelAttachment(@ModelAttribute("KualiForm") UifFormBase uifForm, BindingResult result,
634            HttpServletRequest request, HttpServletResponse response) {
635        // Remove the attached file
636        uifForm.setAttachmentFile(null);
637        return getUIFModelAndView(uifForm);
638    }
639
640    /**
641     * Checks if the given value matches patterns that indicate sensitive data
642     * and if configured to give a warning for sensitive data will prompt the
643     * user to continue.
644     *
645     * @param form
646     * @param request
647     * @param response
648     * @param fieldName - name of field with value being checked
649     * @param fieldValue - value to check for sensitive data
650     * @param caller - method that should be called back from question
651     * @param context - additional context that needs to be passed back with the
652     * question response
653     * @return - view for spring to forward to, or null if processing should
654     *         continue
655     * @throws Exception
656     */
657    protected String checkAndWarnAboutSensitiveData(DocumentFormBase form, HttpServletRequest request,
658            HttpServletResponse response, String fieldName, String fieldValue, String caller,
659            String context) throws Exception {
660
661        String viewName = null;
662        Document document = form.getDocument();
663
664        // TODO: need to move containsSensitiveDataPatternMatch to util class in krad
665//        boolean containsSensitiveData = false;
666//        //boolean containsSensitiveData = WebUtils.containsSensitiveDataPatternMatch(fieldValue);
667//
668//        // check if warning is configured in which case we will prompt, or if
669//        // not business rules will thrown an error
670//        boolean warnForSensitiveData = CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(
671//                KRADConstants.KRAD_NAMESPACE, ParameterConstants.ALL_COMPONENT,
672//                KRADConstants.SystemGroupParameterNames.SENSITIVE_DATA_PATTERNS_WARNING_IND);
673//
674//        // determine if the question has been asked yet
675//        Map<String, String> ticketContext = new HashMap<String, String>();
676//        ticketContext.put(KRADPropertyConstants.DOCUMENT_NUMBER, document.getDocumentNumber());
677//        ticketContext.put(KRADConstants.CALLING_METHOD, caller);
678//        ticketContext.put(KRADPropertyConstants.NAME, fieldName);
679//
680//        boolean questionAsked = GlobalVariables.getUserSession().hasMatchingSessionTicket(
681//                KRADConstants.SENSITIVE_DATA_QUESTION_SESSION_TICKET, ticketContext);
682//
683//        // start in logic for confirming the sensitive data
684//        if (containsSensitiveData && warnForSensitiveData && !questionAsked) {
685//            Object question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
686//            if (question == null || !KRADConstants.DOCUMENT_SENSITIVE_DATA_QUESTION.equals(question)) {
687//
688//                // TODO not ready for question framework yet
689//                /*
690//                     * // question hasn't been asked, prompt to continue return
691//                     * this.performQuestionWithoutInput(mapping, form, request,
692//                     * response, KRADConstants.DOCUMENT_SENSITIVE_DATA_QUESTION,
693//                     * getKualiConfigurationService()
694//                     * .getPropertyValueAsString(RiceKeyConstants
695//                     * .QUESTION_SENSITIVE_DATA_DOCUMENT),
696//                     * KRADConstants.CONFIRMATION_QUESTION, caller, context);
697//                     */
698//                viewName = "ask_user_questions";
699//            } else {
700//                Object buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
701//
702//                // if no button clicked just reload the doc
703//                if (ConfirmationQuestion.NO.equals(buttonClicked)) {
704//                    // TODO figure out what to return
705//                    viewName = "user_says_no";
706//                }
707//
708//                // answered yes, create session ticket so we not to ask question
709//                // again if there are further question requests
710//                SessionTicket ticket = new SessionTicket(KRADConstants.SENSITIVE_DATA_QUESTION_SESSION_TICKET);
711//                ticket.setTicketContext(ticketContext);
712//                GlobalVariables.getUserSession().putSessionTicket(ticket);
713//            }
714//        }
715
716        // returning null will indicate processing should continue (no redirect)
717        return viewName;
718    }
719
720    /**
721     * Convenience method to combine the two lists of ad hoc recipients into one which should be done before
722     * calling any of the document service methods that expect a list of ad hoc recipients
723     *
724     * @param form - document form instance containing the ad hod lists
725     * @return List<AdHocRouteRecipient> combined ad hoc recipients
726     */
727    protected List<AdHocRouteRecipient> combineAdHocRecipients(DocumentFormBase form) {
728        Document document = form.getDocument();
729
730        List<AdHocRouteRecipient> adHocRecipients = new ArrayList<AdHocRouteRecipient>();
731        adHocRecipients.addAll(document.getAdHocRoutePersons());
732        adHocRecipients.addAll(document.getAdHocRouteWorkgroups());
733
734        return adHocRecipients;
735    }
736
737    /**
738     * Convenience method for building authorization exceptions
739     *
740     * @param action - the action that was requested
741     * @param document - document instance the action was requested for
742     */
743    protected DocumentAuthorizationException buildAuthorizationException(String action, Document document) {
744        return new DocumentAuthorizationException(GlobalVariables.getUserSession().getPerson().getPrincipalName(),
745                action, document.getDocumentNumber());
746    }
747
748    public BusinessObjectService getBusinessObjectService() {
749        if (this.businessObjectService == null) {
750            this.businessObjectService = KRADServiceLocator.getBusinessObjectService();
751        }
752        return this.businessObjectService;
753    }
754
755    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
756        this.businessObjectService = businessObjectService;
757    }
758
759    public DataDictionaryService getDataDictionaryService() {
760        if (this.dataDictionaryService == null) {
761            this.dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
762        }
763        return this.dataDictionaryService;
764    }
765
766    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
767        this.dataDictionaryService = dataDictionaryService;
768    }
769
770    public DocumentService getDocumentService() {
771        if (this.documentService == null) {
772            this.documentService = KRADServiceLocatorWeb.getDocumentService();
773        }
774        return this.documentService;
775    }
776
777    public void setDocumentService(DocumentService documentService) {
778        this.documentService = documentService;
779    }
780
781    public DocumentDictionaryService getDocumentDictionaryService() {
782        if (this.documentDictionaryService == null) {
783            this.documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
784        }
785        return this.documentDictionaryService;
786    }
787
788    public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
789        this.documentDictionaryService = documentDictionaryService;
790    }
791
792    public AttachmentService getAttachmentService() {
793        if (attachmentService == null) {
794            attachmentService = KRADServiceLocator.getAttachmentService();
795        }
796        return this.attachmentService;
797    }
798
799    public NoteService getNoteService() {
800        if (noteService == null) {
801            noteService = KRADServiceLocator.getNoteService();
802        }
803
804        return this.noteService;
805    }
806
807    public ConfigurationService getConfigurationService() {
808        return KRADServiceLocator.getKualiConfigurationService();
809    }
810
811}