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