001package org.kuali.rice.krad.service.impl;
002
003import org.apache.commons.lang.time.StopWatch;
004import org.kuali.rice.core.api.CoreApiServiceLocator;
005import org.kuali.rice.core.api.config.ConfigurationException;
006import org.kuali.rice.core.api.config.property.ConfigurationService;
007import org.kuali.rice.core.api.datetime.DateTimeService;
008import org.kuali.rice.core.api.util.RiceKeyConstants;
009import org.kuali.rice.kew.api.WorkflowDocument;
010import org.kuali.rice.kew.api.exception.WorkflowException;
011import org.kuali.rice.kim.api.identity.Person;
012import org.kuali.rice.kim.api.identity.PersonService;
013import org.kuali.rice.kim.api.services.KimApiServiceLocator;
014import org.kuali.rice.krad.UserSession;
015import org.kuali.rice.krad.UserSessionUtils;
016import org.kuali.rice.krad.bo.*;
017import org.kuali.rice.krad.dao.DocumentDao;
018import org.kuali.rice.krad.datadictionary.exception.UnknownDocumentTypeException;
019import org.kuali.rice.krad.document.Document;
020import org.kuali.rice.krad.document.DocumentAuthorizer;
021import org.kuali.rice.krad.document.DocumentPresentationController;
022import org.kuali.rice.krad.exception.DocumentAuthorizationException;
023import org.kuali.rice.krad.exception.ValidationException;
024import org.kuali.rice.krad.maintenance.MaintenanceDocument;
025import org.kuali.rice.krad.maintenance.MaintenanceDocumentBase;
026import org.kuali.rice.krad.rules.rule.event.*;
027import org.kuali.rice.krad.service.*;
028import org.kuali.rice.krad.util.GlobalVariables;
029import org.kuali.rice.krad.util.KRADConstants;
030import org.kuali.rice.krad.util.NoteType;
031import org.kuali.rice.krad.util.ObjectUtils;
032import org.kuali.rice.krad.workflow.service.WorkflowDocumentService;
033import org.springframework.dao.OptimisticLockingFailureException;
034
035import java.lang.reflect.Constructor;
036import java.lang.reflect.InvocationTargetException;
037import java.text.MessageFormat;
038import java.util.*;
039
040/**
041 * Created by gopalp on 7/7/15.
042 */
043public class DocumentServiceImpl implements DocumentService {
044
045    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentServiceImpl.class);
046
047    private DocumentDao documentDao;
048
049    private DateTimeService dateTimeService;
050    private NoteService noteService;
051    private WorkflowDocumentService workflowDocumentService;
052    private BusinessObjectService businessObjectService;
053    private DataDictionaryService dataDictionaryService;
054    private DocumentHeaderService documentHeaderService;
055    private DocumentDictionaryService documentDictionaryService;
056    private PersonService personService;
057    private ConfigurationService kualiConfigurationService;
058
059    /**
060     * @see DocumentService#saveDocument(Document)
061     */
062    @Override
063    public Document saveDocument(Document document) throws WorkflowException, ValidationException {
064        return saveDocument(document, SaveDocumentEvent.class);
065    }
066
067    @Override
068    public Document saveDocument(Document document,
069                                 Class<? extends KualiDocumentEvent> kualiDocumentEventClass) throws WorkflowException, ValidationException {
070        checkForNulls(document);
071        if (kualiDocumentEventClass == null) {
072            throw new IllegalArgumentException("invalid (null) kualiDocumentEventClass");
073        }
074        // if event is not an instance of a SaveDocumentEvent or a SaveOnlyDocumentEvent
075        if (!SaveEvent.class.isAssignableFrom(kualiDocumentEventClass)) {
076            throw new ConfigurationException("The KualiDocumentEvent class '" + kualiDocumentEventClass.getName() +
077                    "' does not implement the class '" + SaveEvent.class.getName() + "'");
078        }
079//        if (!getDocumentActionFlags(document).getCanSave()) {
080//            throw buildAuthorizationException("save", document);
081//        }
082        document.prepareForSave();
083        Document savedDocument = validateAndPersistDocumentAndSaveAdHocRoutingRecipients(document,
084                generateKualiDocumentEvent(document, kualiDocumentEventClass));
085        prepareWorkflowDocument(savedDocument);
086        getWorkflowDocumentService().save(savedDocument.getDocumentHeader().getWorkflowDocument(), null);
087
088        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
089                savedDocument.getDocumentHeader().getWorkflowDocument());
090
091        return savedDocument;
092    }
093
094    private KualiDocumentEvent generateKualiDocumentEvent(Document document,
095                                                          Class<? extends KualiDocumentEvent> eventClass) throws ConfigurationException {
096        String potentialErrorMessage =
097                "Found error trying to generate Kuali Document Event using event class '" + eventClass.getName() +
098                        "' for document " + document.getDocumentNumber();
099
100        try {
101            Constructor<?> usableConstructor = null;
102            List<Object> paramList = new ArrayList<Object>();
103            for (Constructor<?> currentConstructor : eventClass.getConstructors()) {
104                for (Class<?> parameterClass : currentConstructor.getParameterTypes()) {
105                    if (Document.class.isAssignableFrom(parameterClass)) {
106                        usableConstructor = currentConstructor;
107                        paramList.add(document);
108                    } else {
109                        paramList.add(null);
110                    }
111                }
112                if (ObjectUtils.isNotNull(usableConstructor)) {
113                    break;
114                }
115            }
116            if (usableConstructor == null) {
117                throw new RuntimeException("Cannot find a constructor for class '" + eventClass.getName() +
118                        "' that takes in a document parameter");
119            }
120            return (KualiDocumentEvent) usableConstructor.newInstance(paramList.toArray());
121        } catch (SecurityException e) {
122            throw new ConfigurationException(potentialErrorMessage, e);
123        } catch (IllegalArgumentException e) {
124            throw new ConfigurationException(potentialErrorMessage, e);
125        } catch (InstantiationException e) {
126            throw new ConfigurationException(potentialErrorMessage, e);
127        } catch (IllegalAccessException e) {
128            throw new ConfigurationException(potentialErrorMessage, e);
129        } catch (InvocationTargetException e) {
130            throw new ConfigurationException(potentialErrorMessage, e);
131        }
132    }
133
134    /**
135     * @see DocumentService#routeDocument(Document,
136     *      String, List)
137     */
138    @Override
139    public Document routeDocument(Document document, String annotation,
140                                  List<AdHocRouteRecipient> adHocRecipients) throws ValidationException, WorkflowException {
141        checkForNulls(document);
142        //if (!getDocumentActionFlags(document).getCanRoute()) {
143        //    throw buildAuthorizationException("route", document);
144        //}
145        document.prepareForSave();
146        Document savedDocument = validateAndPersistDocument(document, new RouteDocumentEvent(document));
147        prepareWorkflowDocument(savedDocument);
148        getWorkflowDocumentService()
149                .route(savedDocument.getDocumentHeader().getWorkflowDocument(), annotation, adHocRecipients);
150        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
151                savedDocument.getDocumentHeader().getWorkflowDocument());
152        removeAdHocPersonsAndWorkgroups(savedDocument);
153        return savedDocument;
154    }
155
156    /**
157     * @see DocumentService#approveDocument(Document,
158     *      String,
159     *      List)
160     */
161    @Override
162    public Document approveDocument(Document document, String annotation,
163                                    List<AdHocRouteRecipient> adHocRecipients) throws ValidationException, WorkflowException {
164        checkForNulls(document);
165        //if (!getDocumentActionFlags(document).getCanApprove()) {
166        //    throw buildAuthorizationException("approve", document);
167        //}
168        document.prepareForSave();
169        Document savedDocument = validateAndPersistDocument(document, new ApproveDocumentEvent(document));
170        prepareWorkflowDocument(savedDocument);
171        getWorkflowDocumentService()
172                .approve(savedDocument.getDocumentHeader().getWorkflowDocument(), annotation, adHocRecipients);
173        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
174                savedDocument.getDocumentHeader().getWorkflowDocument());
175        removeAdHocPersonsAndWorkgroups(savedDocument);
176        return savedDocument;
177    }
178
179    /**
180     * @see DocumentService#superUserApproveDocument(Document,
181     *      String)
182     */
183    @Override
184    public Document superUserApproveDocument(Document document, String annotation) throws WorkflowException {
185        getDocumentDao().save(document);
186        prepareWorkflowDocument(document);
187        getWorkflowDocumentService().superUserApprove(document.getDocumentHeader().getWorkflowDocument(), annotation);
188        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
189                document.getDocumentHeader().getWorkflowDocument());
190        removeAdHocPersonsAndWorkgroups(document);
191        return document;
192    }
193
194    /**
195     * @see DocumentService#superUserCancelDocument(Document,
196     *      String)
197     */
198    @Override
199    public Document superUserCancelDocument(Document document, String annotation) throws WorkflowException {
200        getDocumentDao().save(document);
201        prepareWorkflowDocument(document);
202        getWorkflowDocumentService().superUserCancel(document.getDocumentHeader().getWorkflowDocument(), annotation);
203        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
204                document.getDocumentHeader().getWorkflowDocument());
205        removeAdHocPersonsAndWorkgroups(document);
206        return document;
207    }
208
209    /**
210     * @see DocumentService#superUserCancelDocument(Document,
211     *      String)
212     */
213    @Override
214    public Document superUserDisapproveDocument(Document document, String annotation) throws WorkflowException {
215        getDocumentDao().save(document);
216        return superUserDisapproveDocumentWithoutSaving(document, annotation);
217    }
218
219    /**
220     * @see DocumentService#superUserCancelDocument(Document,
221     *      String)
222     */
223    @Override
224    public Document superUserDisapproveDocumentWithoutSaving(Document document, String annotation) throws WorkflowException {
225        prepareWorkflowDocument(document);
226        getWorkflowDocumentService()
227                .superUserDisapprove(document.getDocumentHeader().getWorkflowDocument(), annotation);
228        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
229                document.getDocumentHeader().getWorkflowDocument());
230        removeAdHocPersonsAndWorkgroups(document);
231        return document;
232    }
233
234
235    /**
236     * @see DocumentService#disapproveDocument(Document,
237     *      String)
238     */
239    @Override
240    public Document disapproveDocument(Document document, String annotation) throws Exception {
241        checkForNulls(document);
242
243        Note note = createNoteFromDocument(document, annotation);
244        //if note type is BO, override and link disapprove notes to Doc Header
245        if (document.getNoteType().equals(NoteType.BUSINESS_OBJECT)) {
246            note.setNoteTypeCode(NoteType.DOCUMENT_HEADER.getCode());
247            note.setRemoteObjectIdentifier(document.getDocumentHeader().getObjectId());
248        }
249        document.addNote(note);
250
251        //SAVE THE NOTE
252        //Note: This save logic is replicated here and in KualiDocumentAction, when to save (based on doc state) should be moved
253        //      into a doc service method
254        getNoteService().save(note);
255
256        prepareWorkflowDocument(document);
257        getWorkflowDocumentService().disapprove(document.getDocumentHeader().getWorkflowDocument(), annotation);
258        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
259                document.getDocumentHeader().getWorkflowDocument());
260        removeAdHocPersonsAndWorkgroups(document);
261        return document;
262    }
263
264    /**
265     * @see DocumentService#cancelDocument(Document,
266     *      String)
267     */
268    @Override
269    public Document cancelDocument(Document document, String annotation) throws WorkflowException {
270        checkForNulls(document);
271        //if (!getDocumentActionFlags(document).getCanCancel()) {
272        //    throw buildAuthorizationException("cancel", document);
273        //}
274        if (document instanceof MaintenanceDocument) {
275            MaintenanceDocument maintDoc = ((MaintenanceDocument) document);
276            if (maintDoc.getOldMaintainableObject() != null &&
277                    (maintDoc.getOldMaintainableObject().getDataObject() instanceof BusinessObject)) {
278                ((BusinessObject) maintDoc.getOldMaintainableObject().getDataObject()).refresh();
279            }
280
281            if (maintDoc.getNewMaintainableObject().getDataObject() instanceof BusinessObject) {
282                ((BusinessObject) maintDoc.getNewMaintainableObject().getDataObject()).refresh();
283            }
284        }
285        prepareWorkflowDocument(document);
286        getWorkflowDocumentService().cancel(document.getDocumentHeader().getWorkflowDocument(), annotation);
287        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
288                document.getDocumentHeader().getWorkflowDocument());
289        //getBusinessObjectService().delete(document.getAdHocRoutePersons());
290        //getBusinessObjectService().delete(document.getAdHocRouteWorkgroups());
291        removeAdHocPersonsAndWorkgroups(document);
292        return document;
293    }
294
295    @Override
296    public Document recallDocument(Document document, String annotation, boolean cancel) throws WorkflowException {
297        checkForNulls(document);
298
299        Note note = createNoteFromDocument(document, annotation);
300        document.addNote(note);
301        getNoteService().save(note);
302
303        prepareWorkflowDocument(document);
304        getWorkflowDocumentService().recall(document.getDocumentHeader().getWorkflowDocument(), annotation, cancel);
305        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
306                document.getDocumentHeader().getWorkflowDocument());
307        removeAdHocPersonsAndWorkgroups(document);
308        return document;
309    }
310
311    /**
312     * @see DocumentService#acknowledgeDocument(Document,
313     *      String,
314     *      List)
315     */
316    @Override
317    public Document acknowledgeDocument(Document document, String annotation,
318                                        List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException {
319        checkForNulls(document);
320        //if (!getDocumentActionFlags(document).getCanAcknowledge()) {
321        //    throw buildAuthorizationException("acknowledge", document);
322        //}
323        prepareWorkflowDocument(document);
324        getWorkflowDocumentService()
325                .acknowledge(document.getDocumentHeader().getWorkflowDocument(), annotation, adHocRecipients);
326        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
327                document.getDocumentHeader().getWorkflowDocument());
328        removeAdHocPersonsAndWorkgroups(document);
329        return document;
330    }
331
332    /**
333     * @see DocumentService#blanketApproveDocument(Document,
334     *      String,
335     *      List)
336     */
337    @Override
338    public Document blanketApproveDocument(Document document, String annotation,
339                                           List<AdHocRouteRecipient> adHocRecipients) throws ValidationException, WorkflowException {
340        checkForNulls(document);
341        //if (!getDocumentActionFlags(document).getCanBlanketApprove()) {
342        //    throw buildAuthorizationException("blanket approve", document);
343        //}
344        document.prepareForSave();
345        Document savedDocument = validateAndPersistDocument(document, new BlanketApproveDocumentEvent(document));
346        prepareWorkflowDocument(savedDocument);
347        getWorkflowDocumentService()
348                .blanketApprove(savedDocument.getDocumentHeader().getWorkflowDocument(), annotation, adHocRecipients);
349        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
350                savedDocument.getDocumentHeader().getWorkflowDocument());
351        removeAdHocPersonsAndWorkgroups(savedDocument);
352        return savedDocument;
353    }
354
355    /**
356     * @see DocumentService#clearDocumentFyi(Document,
357     *      List)
358     */
359    @Override
360    public Document clearDocumentFyi(Document document,
361                                     List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException {
362        checkForNulls(document);
363        // populate document content so searchable attributes will be indexed properly
364        document.populateDocumentForRouting();
365        getWorkflowDocumentService().clearFyi(document.getDocumentHeader().getWorkflowDocument(), adHocRecipients);
366        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
367                document.getDocumentHeader().getWorkflowDocument());
368        removeAdHocPersonsAndWorkgroups(document);
369        return document;
370    }
371
372    /**
373     * @see DocumentService#completeDocument(Document,
374     *      String,
375     *      List)
376     */
377    @Override
378    public Document completeDocument(Document document, String annotation,
379                                     List adHocRecipients) throws WorkflowException {
380        checkForNulls(document);
381
382        document.prepareForSave();
383        validateAndPersistDocument(document, new CompleteDocumentEvent(document));
384
385        prepareWorkflowDocument(document);
386        getWorkflowDocumentService().complete(document.getDocumentHeader().getWorkflowDocument(), annotation,
387                adHocRecipients);
388
389        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
390                document.getDocumentHeader().getWorkflowDocument());
391
392        removeAdHocPersonsAndWorkgroups(document);
393
394        return document;
395    }
396
397    protected void checkForNulls(Document document) {
398        if (document == null) {
399            throw new IllegalArgumentException("invalid (null) document");
400        }
401        if (document.getDocumentNumber() == null) {
402            throw new IllegalStateException("invalid (null) documentHeaderId");
403        }
404    }
405
406    private Document validateAndPersistDocumentAndSaveAdHocRoutingRecipients(Document document,
407                                                                             KualiDocumentEvent event) {
408        /*
409         * Using this method to wrap validateAndPersistDocument to keep everything in one transaction. This avoids modifying the
410         * signature on validateAndPersistDocument method
411         */
412        List<AdHocRouteRecipient> adHocRoutingRecipients = new ArrayList<AdHocRouteRecipient>();
413        adHocRoutingRecipients.addAll(document.getAdHocRoutePersons());
414        adHocRoutingRecipients.addAll(document.getAdHocRouteWorkgroups());
415
416        for (AdHocRouteRecipient recipient : adHocRoutingRecipients) {
417            recipient.setdocumentNumber(document.getDocumentNumber());
418        }
419        Map<String, String> criteria = new HashMap<String, String>();
420        criteria.put("documentNumber", document.getDocumentNumber());
421        getBusinessObjectService().deleteMatching(AdHocRouteRecipient.class, criteria);
422
423        getBusinessObjectService().save(adHocRoutingRecipients);
424        return validateAndPersistDocument(document, event);
425    }
426
427    /**
428     * @see DocumentService#documentExists(String)
429     */
430    @Override
431    public boolean documentExists(String documentHeaderId) {
432        // validate parameters
433        if (org.apache.commons.lang.StringUtils.isBlank(documentHeaderId)) {
434            throw new IllegalArgumentException("invalid (blank) documentHeaderId");
435        }
436
437        boolean internalUserSession = false;
438        try {
439            // KFSMI-2543 - allowed method to run without a user session so it can be used
440            // by workflow processes
441            if (GlobalVariables.getUserSession() == null) {
442                internalUserSession = true;
443                GlobalVariables.setUserSession(new UserSession(KRADConstants.SYSTEM_USER));
444                GlobalVariables.clear();
445            }
446
447            // look for workflowDocumentHeader, since that supposedly won't break the transaction
448            if (getWorkflowDocumentService().workflowDocumentExists(documentHeaderId)) {
449                // look for docHeaderId, since that fails without breaking the transaction
450                return getDocumentHeaderService().getDocumentHeaderById(documentHeaderId) != null;
451            }
452
453            return false;
454        } finally {
455            // if a user session was established for this call, clear it our
456            if (internalUserSession) {
457                GlobalVariables.clear();
458                GlobalVariables.setUserSession(null);
459            }
460        }
461    }
462
463    /**
464     * Creates a new document by class.
465     *
466     * @see DocumentService#getNewDocument(Class)
467     */
468    @Override
469    public Document getNewDocument(Class<? extends Document> documentClass) throws WorkflowException {
470        if (documentClass == null) {
471            throw new IllegalArgumentException("invalid (null) documentClass");
472        }
473        if (!Document.class.isAssignableFrom(documentClass)) {
474            throw new IllegalArgumentException("invalid (non-Document) documentClass");
475        }
476
477        String documentTypeName = getDataDictionaryService().getDocumentTypeNameByClass(documentClass);
478        if (org.apache.commons.lang.StringUtils.isBlank(documentTypeName)) {
479            throw new UnknownDocumentTypeException(
480                    "unable to get documentTypeName for unknown documentClass '" + documentClass.getName() + "'");
481        }
482        return getNewDocument(documentTypeName);
483    }
484
485    /**
486     * Creates a new document by document type name. The principal name
487     * passed in will be used as the document initiator.  If the  initiatorPrincipalNm
488     * is null or blank, the current user will be used.
489     *
490     * @see DocumentService#getNewDocument(String, String)
491     */
492    @Override
493    public Document getNewDocument(String documentTypeName, String initiatorPrincipalNm) throws WorkflowException {
494
495        // argument validation
496        String watchName = "DocumentServiceImpl.getNewDocument";
497        StopWatch watch = new StopWatch();
498        watch.start();
499        if (LOG.isDebugEnabled()) {
500            LOG.debug(watchName + ": started");
501        }
502        if (org.apache.commons.lang.StringUtils.isBlank(documentTypeName)) {
503            throw new IllegalArgumentException("invalid (blank) documentTypeName");
504        }
505        if (GlobalVariables.getUserSession() == null) {
506            throw new IllegalStateException(
507                    "GlobalVariables must be populated with a valid UserSession before a new document can be created");
508        }
509
510        // get the class for this docTypeName
511        Class<? extends Document> documentClass = getDocumentClassByTypeName(documentTypeName);
512
513        // get the initiator
514        Person initiator = null;
515        if (org.apache.commons.lang.StringUtils.isBlank(initiatorPrincipalNm)) {
516            initiator = GlobalVariables.getUserSession().getPerson();
517        } else {
518            initiator = KimApiServiceLocator.getPersonService().getPersonByPrincipalName(initiatorPrincipalNm);
519            if (ObjectUtils.isNull(initiator)) {
520                initiator = GlobalVariables.getUserSession().getPerson();
521            }
522        }
523
524        // get the authorization
525        DocumentAuthorizer documentAuthorizer = getDocumentDictionaryService().getDocumentAuthorizer(documentTypeName);
526        DocumentPresentationController documentPresentationController =
527                getDocumentDictionaryService().getDocumentPresentationController(documentTypeName);
528        // make sure this person is authorized to initiate
529        LOG.debug("calling canInitiate from getNewDocument()");
530        if (!documentPresentationController.canInitiate(documentTypeName) ||
531                !documentAuthorizer.canInitiate(documentTypeName, initiator)) {
532            throw new DocumentAuthorizationException(initiator.getPrincipalName(), "initiate", documentTypeName);
533        }
534
535        // initiate new workflow entry, get the workflow doc
536        WorkflowDocument workflowDocument = getWorkflowDocumentService().createWorkflowDocument(documentTypeName, initiator);
537        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(), workflowDocument);
538
539        // create a new document header object
540        DocumentHeader documentHeader = null;
541        try {
542            // create a new document header object
543            Class<? extends DocumentHeader> documentHeaderClass =
544                    getDocumentHeaderService().getDocumentHeaderBaseClass();
545            documentHeader = documentHeaderClass.newInstance();
546            documentHeader.setWorkflowDocument(workflowDocument);
547            documentHeader.setDocumentNumber(workflowDocument.getDocumentId());
548            // status and notes are initialized correctly in the constructor
549        } catch (IllegalAccessException e) {
550            throw new RuntimeException("Error instantiating DocumentHeader", e);
551        } catch (InstantiationException e) {
552            throw new RuntimeException("Error instantiating DocumentHeader", e);
553        }
554
555        // build Document of specified type
556        Document document = null;
557        try {
558            // all maintenance documents have same class
559            if (MaintenanceDocumentBase.class.isAssignableFrom(documentClass)) {
560                Class<?>[] defaultConstructor = new Class[]{String.class};
561                Constructor<? extends Document> cons = documentClass.getConstructor(defaultConstructor);
562                if (ObjectUtils.isNull(cons)) {
563                    throw new ConfigurationException(
564                            "Could not find constructor with document type name parameter needed for Maintenance Document Base class");
565                }
566                document = cons.newInstance(documentTypeName);
567            } else {
568                // non-maintenance document
569                document = documentClass.newInstance();
570            }
571        } catch (IllegalAccessException e) {
572            throw new RuntimeException("Error instantiating Document", e);
573        } catch (InstantiationException e) {
574            throw new RuntimeException("Error instantiating Document", e);
575        } catch (SecurityException e) {
576            throw new RuntimeException("Error instantiating Maintenance Document", e);
577        } catch (NoSuchMethodException e) {
578            throw new RuntimeException(
579                    "Error instantiating Maintenance Document: No constructor with String parameter found", e);
580        } catch (IllegalArgumentException e) {
581            throw new RuntimeException("Error instantiating Maintenance Document", e);
582        } catch (InvocationTargetException e) {
583            throw new RuntimeException("Error instantiating Maintenance Document", e);
584        }
585
586        document.setDocumentHeader(documentHeader);
587        document.setDocumentNumber(documentHeader.getDocumentNumber());
588
589        watch.stop();
590        if (LOG.isDebugEnabled()) {
591            LOG.debug(watchName + ": " + watch.toString());
592        }
593
594        return document;
595    }
596
597    /**
598     * Creates a new document by document type name.
599     *
600     * @see DocumentService#getNewDocument(String)
601     */
602    @Override
603    public Document getNewDocument(String documentTypeName) throws WorkflowException {
604        return getNewDocument(documentTypeName, null);
605    }
606
607
608    /**
609     * This is temporary until workflow 2.0 and reads from a table to get documents whose status has changed to A
610     * (approved - no
611     * outstanding approval actions requested)
612     *
613     * @param documentHeaderId
614     * @return Document
615     * @throws WorkflowException
616     */
617    @Override
618    public Document getByDocumentHeaderId(String documentHeaderId) throws WorkflowException {
619        if (documentHeaderId == null) {
620            throw new IllegalArgumentException("invalid (null) documentHeaderId");
621        }
622        boolean internalUserSession = false;
623        try {
624            // KFSMI-2543 - allowed method to run without a user session so it can be used
625            // by workflow processes
626            if (GlobalVariables.getUserSession() == null) {
627                internalUserSession = true;
628                GlobalVariables.setUserSession(new UserSession(KRADConstants.SYSTEM_USER));
629                GlobalVariables.clear();
630            }
631
632            WorkflowDocument workflowDocument = null;
633
634            if (LOG.isDebugEnabled()) {
635                LOG.debug("Retrieving doc id: " + documentHeaderId + " from workflow service.");
636            }
637            workflowDocument = getWorkflowDocumentService()
638                    .loadWorkflowDocument(documentHeaderId, GlobalVariables.getUserSession().getPerson());
639            UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(), workflowDocument);
640
641            Class<? extends Document> documentClass = getDocumentClassByTypeName(workflowDocument.getDocumentTypeName());
642
643            // retrieve the Document
644            Document document = getDocumentDao().findByDocumentHeaderId(documentClass, documentHeaderId);
645
646            return postProcessDocument(documentHeaderId, workflowDocument, document);
647        } finally {
648            // if a user session was established for this call, clear it out
649            if (internalUserSession) {
650                GlobalVariables.clear();
651                GlobalVariables.setUserSession(null);
652            }
653        }
654    }
655
656    /**
657     * @see DocumentService#getByDocumentHeaderIdSessionless(String)
658     */
659    @Override
660    public Document getByDocumentHeaderIdSessionless(String documentHeaderId) throws WorkflowException {
661        if (documentHeaderId == null) {
662            throw new IllegalArgumentException("invalid (null) documentHeaderId");
663        }
664
665        WorkflowDocument workflowDocument = null;
666
667        if (LOG.isDebugEnabled()) {
668            LOG.debug("Retrieving doc id: " + documentHeaderId + " from workflow service.");
669        }
670
671        Person person = getPersonService().getPersonByPrincipalName(KRADConstants.SYSTEM_USER);
672        workflowDocument = workflowDocumentService.loadWorkflowDocument(documentHeaderId, person);
673
674        Class<? extends Document> documentClass = getDocumentClassByTypeName(workflowDocument.getDocumentTypeName());
675
676        // retrieve the Document
677        Document document = getDocumentDao().findByDocumentHeaderId(documentClass, documentHeaderId);
678
679        return postProcessDocument(documentHeaderId, workflowDocument, document);
680    }
681
682    private Class<? extends Document> getDocumentClassByTypeName(String documentTypeName) {
683        if (org.apache.commons.lang.StringUtils.isBlank(documentTypeName)) {
684            throw new IllegalArgumentException("invalid (blank) documentTypeName");
685        }
686
687        Class<? extends Document> clazz = getDataDictionaryService().getDocumentClassByTypeName(documentTypeName);
688        if (clazz == null) {
689            throw new UnknownDocumentTypeException(
690                    "unable to get class for unknown documentTypeName '" + documentTypeName + "'");
691        }
692        return clazz;
693    }
694
695    /**
696     * Loads the Notes for the note target on this Document.
697     *
698     * @param document the document for which to load the notes
699     */
700    protected void loadNotes(Document document) {
701        if (isNoteTargetReady(document)) {
702            List<Note> notes = new ArrayList<Note>();
703            if (org.apache.commons.lang.StringUtils.isNotBlank(document.getNoteTarget().getObjectId())) {
704                notes.addAll(getNoteService().getByRemoteObjectId(document.getNoteTarget().getObjectId()));
705            }
706            //notes created on 'disapprove' are linked to Doc Header, so this checks that even if notetype = BO
707            if (document.getNoteType().equals(NoteType.BUSINESS_OBJECT)
708                    && document.getDocumentHeader().getWorkflowDocument().isDisapproved()) {
709                notes.addAll(getNoteService().getByRemoteObjectId(document.getDocumentHeader().getObjectId()));
710            }
711
712            // KULRNE-5692 - force a refresh of the attachments
713            // they are not (non-updateable) references and don't seem to update properly upon load
714            for (Note note : notes) {
715                note.refreshReferenceObject("attachment");
716            }
717            document.setNotes(notes);
718        }
719    }
720
721    /**
722     * Performs required post-processing for every document from the documentDao
723     *
724     * @param documentHeaderId
725     * @param workflowDocument
726     * @param document
727     */
728    private Document postProcessDocument(String documentHeaderId, WorkflowDocument workflowDocument, Document document) {
729        if (document != null) {
730            document.getDocumentHeader().setWorkflowDocument(workflowDocument);
731            document.processAfterRetrieve();
732            loadNotes(document);
733        }
734        return document;
735    }
736
737    /**
738     * The default implementation - this retrieves all documents by a list of documentHeader for a given class.
739     *
740     * @see DocumentService#getDocumentsByListOfDocumentHeaderIds(Class,
741     *      List)
742     */
743    @Override
744    public List<Document> getDocumentsByListOfDocumentHeaderIds(Class<? extends Document> documentClass,
745                                                                List<String> documentHeaderIds) throws WorkflowException {
746        // validate documentHeaderIdList and contents
747        if (documentHeaderIds == null) {
748            throw new IllegalArgumentException("invalid (null) documentHeaderId list");
749        }
750        int index = 0;
751        for (String documentHeaderId : documentHeaderIds) {
752            if (org.apache.commons.lang.StringUtils.isBlank(documentHeaderId)) {
753                throw new IllegalArgumentException("invalid (blank) documentHeaderId at list index " + index);
754            }
755            index++;
756        }
757
758        boolean internalUserSession = false;
759        try {
760            // KFSMI-2543 - allowed method to run without a user session so it can be used
761            // by workflow processes
762            if (GlobalVariables.getUserSession() == null) {
763                internalUserSession = true;
764                GlobalVariables.setUserSession(new UserSession(KRADConstants.SYSTEM_USER));
765                GlobalVariables.clear();
766            }
767
768            // retrieve all documents that match the document header ids
769            List<? extends Document> rawDocuments =
770                    getDocumentDao().findByDocumentHeaderIds(documentClass, documentHeaderIds);
771
772            // post-process them
773            List<Document> documents = new ArrayList<Document>();
774            for (Document document : rawDocuments) {
775                WorkflowDocument workflowDocument = getWorkflowDocumentService().loadWorkflowDocument(document.getDocumentNumber(), GlobalVariables.getUserSession().getPerson());
776
777                document = postProcessDocument(document.getDocumentNumber(), workflowDocument, document);
778                documents.add(document);
779            }
780            return documents;
781        } finally {
782            // if a user session was established for this call, clear it our
783            if (internalUserSession) {
784                GlobalVariables.clear();
785                GlobalVariables.setUserSession(null);
786            }
787        }
788    }
789
790    /* Helper Methods */
791
792    /**
793     * Validates and persists a document.
794     */
795    @Override
796    public Document validateAndPersistDocument(Document document, KualiDocumentEvent event) throws ValidationException {
797        if (document == null) {
798            LOG.error("document passed to validateAndPersist was null");
799            throw new IllegalArgumentException("invalid (null) document");
800        }
801        if (LOG.isDebugEnabled()) {
802            LOG.debug("validating and preparing to persist document " + document.getDocumentNumber());
803        }
804
805        // document.validateBusinessRules(event);
806        document.prepareForSave(event);
807
808        // save the document
809        Document savedDocument = null;
810        try {
811            if (LOG.isInfoEnabled()) {
812                LOG.info("storing document " + document.getDocumentNumber());
813            }
814            savedDocument = getDocumentDao().save(document);
815        } catch (OptimisticLockingFailureException e) {
816            LOG.error("exception encountered on store of document " + e.getMessage());
817            throw e;
818        }
819
820        boolean notesSaved = saveDocumentNotes(document);
821        if (!notesSaved) {
822            if (LOG.isInfoEnabled()) {
823                LOG.info(
824                        "Notes not saved during validateAndPersistDocument, likely means that note save needs to be deferred because note target is not ready.");
825            }
826        }
827
828        savedDocument.postProcessSave(event);
829
830        return savedDocument;
831    }
832
833    /**
834     * Sets the title and app document id in the flex document
835     *
836     * @param document
837     * @throws WorkflowException
838     */
839    @Override
840    public void prepareWorkflowDocument(Document document) throws WorkflowException {
841        // populate document content so searchable attributes will be indexed properly
842        document.populateDocumentForRouting();
843
844        // make sure we push the document title into the workflowDocument
845        populateDocumentTitle(document);
846
847        // make sure we push the application document id into the workflowDocument
848        populateApplicationDocumentId(document);
849    }
850
851    /**
852     * This method will grab the generated document title from the document and add it to the workflowDocument so that
853     * it gets pushed into
854     * workflow when routed.
855     *
856     * @param document
857     * @throws WorkflowException
858     */
859    private void populateDocumentTitle(Document document) throws WorkflowException {
860        String documentTitle = document.getDocumentTitle();
861        if (org.apache.commons.lang.StringUtils.isNotBlank(documentTitle)) {
862            document.getDocumentHeader().getWorkflowDocument().setTitle(documentTitle);
863        }
864    }
865
866    /**
867     * This method will grab the organization document number from the document and add it to the workflowDocument so
868     * that it gets pushed
869     * into workflow when routed.
870     *
871     * @param document
872     */
873    private void populateApplicationDocumentId(Document document) {
874        String organizationDocumentNumber = document.getDocumentHeader().getOrganizationDocumentNumber();
875        if (org.apache.commons.lang.StringUtils.isNotBlank(organizationDocumentNumber)) {
876            document.getDocumentHeader().getWorkflowDocument().setApplicationDocumentId(organizationDocumentNumber);
877        }
878    }
879
880    /**
881     * This is to allow for updates of document statuses and other related requirements for updates outside of the
882     * initial save and
883     * route
884     */
885    @Override
886    public Document updateDocument(Document document) {
887        checkForNulls(document);
888        return getDocumentDao().save(document);
889    }
890
891    /**
892     * @see DocumentService#createNoteFromDocument(Document,
893     *      String)
894     */
895    @Override
896    public Note createNoteFromDocument(Document document, String text) {
897        Note note = new Note();
898
899        note.setNotePostedTimestamp(getDateTimeService().getCurrentTimestamp());
900        note.setVersionNumber(Long.valueOf(1));
901        note.setNoteText(text);
902        note.setNoteTypeCode(document.getNoteType().getCode());
903
904        PersistableBusinessObject bo = document.getNoteTarget();
905        // TODO gah! this is awful
906        Person kualiUser = GlobalVariables.getUserSession().getPerson();
907        if (kualiUser == null) {
908            throw new IllegalStateException("Current UserSession has a null Person.");
909        }
910        return bo == null ? null : getNoteService().createNote(note, bo, kualiUser.getPrincipalId());
911    }
912
913    /**
914     * @see DocumentService#saveDocumentNotes(Document)
915     */
916    @Override
917    public boolean saveDocumentNotes(Document document) {
918        if (isNoteTargetReady(document)) {
919            List<Note> notes = document.getNotes();
920            for (Note note : document.getNotes()) {
921                linkNoteRemoteObjectId(note, document.getNoteTarget());
922            }
923            getNoteService().saveNoteList(notes);
924            return true;
925        }
926        return false;
927    }
928
929    /**
930     * @see DocumentService
931     */
932    @Override
933    public void sendNoteRouteNotification(Document document, Note note, Person sender) throws WorkflowException {
934        AdHocRouteRecipient routeRecipient = note.getAdHocRouteRecipient();
935
936        // build notification request
937        Person requestedUser = this.getPersonService().getPersonByPrincipalName(routeRecipient.getId());
938        String senderName = sender.getFirstName() + " " + sender.getLastName();
939        String requestedName = requestedUser.getFirstName() + " " + requestedUser.getLastName();
940
941        String notificationText =
942                kualiConfigurationService.getPropertyValueAsString(
943                        RiceKeyConstants.MESSAGE_NOTE_NOTIFICATION_ANNOTATION);
944        if (org.apache.commons.lang.StringUtils.isBlank(notificationText)) {
945            throw new RuntimeException(
946                    "No annotation message found for note notification. Message needs added to application resources with key:" +
947                            RiceKeyConstants.MESSAGE_NOTE_NOTIFICATION_ANNOTATION);
948        }
949        notificationText =
950                MessageFormat.format(notificationText, new Object[]{senderName, requestedName, note.getNoteText()});
951
952        List<AdHocRouteRecipient> routeRecipients = new ArrayList<AdHocRouteRecipient>();
953        routeRecipients.add(routeRecipient);
954
955        workflowDocumentService
956                .sendWorkflowNotification(document.getDocumentHeader().getWorkflowDocument(), notificationText,
957                        routeRecipients, KRADConstants.NOTE_WORKFLOW_NOTIFICATION_REQUEST_LABEL);
958
959        // clear recipient allowing an notification to be sent to another person
960        note.setAdHocRouteRecipient(new AdHocRoutePerson());
961    }
962
963    /**
964     * Determines if the given document's note target is ready for notes to be
965     * attached and persisted against it.  This method verifies that the document's
966     * note target is non-null as well as checking that it has a non-empty object id.
967     *
968     * @param document the document on which to check for note target readiness
969     * @return true if the note target is ready, false otherwise
970     */
971    protected boolean isNoteTargetReady(Document document) {
972
973        //special case for disappoved documents
974        if (document.getDocumentHeader().getWorkflowDocument().isDisapproved()) {
975            return true;
976        }
977        PersistableBusinessObject noteTarget = document.getNoteTarget();
978        if (noteTarget == null || org.apache.commons.lang.StringUtils.isBlank(noteTarget.getObjectId())) {
979            return false;
980        }
981        return true;
982    }
983
984    private void linkNoteRemoteObjectId(Note note, PersistableBusinessObject noteTarget) {
985        String objectId = noteTarget.getObjectId();
986        if (org.apache.commons.lang.StringUtils.isBlank(objectId)) {
987            throw new IllegalStateException(
988                    "Attempted to link a Note with a PersistableBusinessObject with no object id");
989        }
990        note.setRemoteObjectIdentifier(noteTarget.getObjectId());
991    }
992
993    /**
994     * @see DocumentService#sendAdHocRequests(Document, String, List)
995     */
996    @Override
997    public void sendAdHocRequests(Document document, String annotation,
998                                  List<AdHocRouteRecipient> adHocRecipients) throws WorkflowException {
999        prepareWorkflowDocument(document);
1000        getWorkflowDocumentService()
1001                .sendWorkflowNotification(document.getDocumentHeader().getWorkflowDocument(), annotation,
1002                        adHocRecipients);
1003        UserSessionUtils.addWorkflowDocument(GlobalVariables.getUserSession(),
1004                document.getDocumentHeader().getWorkflowDocument());
1005        //getBusinessObjectService().delete(document.getAdHocRoutePersons());
1006        //getBusinessObjectService().delete(document.getAdHocRouteWorkgroups());
1007        removeAdHocPersonsAndWorkgroups(document);
1008    }
1009
1010    private void removeAdHocPersonsAndWorkgroups(Document document) {
1011        List<AdHocRoutePerson> adHocRoutePersons = new ArrayList<AdHocRoutePerson>();
1012        List<AdHocRouteWorkgroup> adHocRouteWorkgroups = new ArrayList<AdHocRouteWorkgroup>();
1013        getBusinessObjectService().delete(document.getAdHocRoutePersons());
1014        getBusinessObjectService().delete(document.getAdHocRouteWorkgroups());
1015        document.setAdHocRoutePersons(adHocRoutePersons);
1016        document.setAdHocRouteWorkgroups(adHocRouteWorkgroups);
1017    }
1018
1019    public void setDateTimeService(DateTimeService dateTimeService) {
1020        this.dateTimeService = dateTimeService;
1021    }
1022
1023    protected DateTimeService getDateTimeService() {
1024        if (this.dateTimeService == null) {
1025            this.dateTimeService = CoreApiServiceLocator.getDateTimeService();
1026        }
1027        return this.dateTimeService;
1028    }
1029
1030    public void setNoteService(NoteService noteService) {
1031        this.noteService = noteService;
1032    }
1033
1034    protected NoteService getNoteService() {
1035        if (this.noteService == null) {
1036            this.noteService = KRADServiceLocator.getNoteService();
1037        }
1038        return this.noteService;
1039    }
1040
1041    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
1042        this.businessObjectService = businessObjectService;
1043    }
1044
1045    protected BusinessObjectService getBusinessObjectService() {
1046        if (this.businessObjectService == null) {
1047            this.businessObjectService = KRADServiceLocator.getBusinessObjectService();
1048        }
1049        return this.businessObjectService;
1050    }
1051
1052    public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
1053        this.workflowDocumentService = workflowDocumentService;
1054    }
1055
1056    protected WorkflowDocumentService getWorkflowDocumentService() {
1057        if (this.workflowDocumentService == null) {
1058            this.workflowDocumentService = KRADServiceLocatorWeb.getWorkflowDocumentService();
1059        }
1060        return this.workflowDocumentService;
1061    }
1062
1063    public void setDocumentDao(DocumentDao documentDao) {
1064        this.documentDao = documentDao;
1065    }
1066
1067    protected DocumentDao getDocumentDao() {
1068        return documentDao;
1069    }
1070
1071    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1072        this.dataDictionaryService = dataDictionaryService;
1073    }
1074
1075    protected DataDictionaryService getDataDictionaryService() {
1076        if (this.dataDictionaryService == null) {
1077            this.dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
1078        }
1079        return this.dataDictionaryService;
1080    }
1081
1082    public void setDocumentHeaderService(DocumentHeaderService documentHeaderService) {
1083        this.documentHeaderService = documentHeaderService;
1084    }
1085
1086    protected DocumentHeaderService getDocumentHeaderService() {
1087        if (this.documentHeaderService == null) {
1088            this.documentHeaderService = KRADServiceLocatorWeb.getDocumentHeaderService();
1089        }
1090        return this.documentHeaderService;
1091    }
1092
1093    protected DocumentDictionaryService getDocumentDictionaryService() {
1094        if (documentDictionaryService == null) {
1095            documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1096        }
1097        return documentDictionaryService;
1098    }
1099
1100    public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1101        this.documentDictionaryService = documentDictionaryService;
1102    }
1103
1104    public PersonService getPersonService() {
1105        if (personService == null) {
1106            personService = KimApiServiceLocator.getPersonService();
1107        }
1108        return personService;
1109    }
1110
1111    public void setKualiConfigurationService(ConfigurationService kualiConfigurationService) {
1112        this.kualiConfigurationService = kualiConfigurationService;
1113    }
1114}