001package org.kuali.rice.krad.maintenance;
002
003import java.io.IOException;
004import java.io.StringReader;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collections;
008import java.util.List;
009
010import javax.persistence.CascadeType;
011import javax.persistence.Column;
012import javax.persistence.Entity;
013import javax.persistence.FetchType;
014import javax.persistence.JoinColumn;
015import javax.persistence.Lob;
016import javax.persistence.ManyToOne;
017import javax.persistence.OneToMany;
018import javax.persistence.OneToOne;
019import javax.persistence.Table;
020import javax.persistence.Transient;
021import javax.persistence.UniqueConstraint;
022import javax.xml.parsers.DocumentBuilder;
023import javax.xml.parsers.DocumentBuilderFactory;
024import javax.xml.parsers.ParserConfigurationException;
025
026import org.apache.commons.collections.CollectionUtils;
027import org.apache.commons.lang.StringUtils;
028import org.kuali.rice.core.api.config.property.ConfigContext;
029import org.kuali.rice.core.api.mo.common.GloballyUnique;
030import org.kuali.rice.kew.api.KewApiServiceLocator;
031import org.kuali.rice.kew.api.WorkflowDocument;
032import org.kuali.rice.kew.api.doctype.DocumentType;
033import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
034import org.kuali.rice.kim.api.identity.Person;
035import org.kuali.rice.kim.api.services.KimApiServiceLocator;
036import org.kuali.rice.krad.bo.DocumentAttachment;
037import org.kuali.rice.krad.bo.DocumentHeader;
038import org.kuali.rice.krad.bo.MultiDocumentAttachment;
039import org.kuali.rice.krad.bo.Note;
040import org.kuali.rice.krad.bo.PersistableAttachment;
041import org.kuali.rice.krad.bo.PersistableAttachmentList;
042import org.kuali.rice.krad.data.KradDataServiceLocator;
043import org.kuali.rice.krad.datadictionary.DocumentEntry;
044import org.kuali.rice.krad.datadictionary.WorkflowAttributes;
045import org.kuali.rice.krad.datadictionary.WorkflowProperties;
046import org.kuali.rice.krad.document.DocumentBase;
047import org.kuali.rice.krad.document.SessionDocument;
048import org.kuali.rice.krad.exception.PessimisticLockingException;
049import org.kuali.rice.krad.exception.ValidationException;
050import org.kuali.rice.krad.rules.rule.event.DocumentEvent;
051import org.kuali.rice.krad.rules.rule.event.SaveDocumentEvent;
052import org.kuali.rice.krad.service.BusinessObjectSerializerService;
053import org.kuali.rice.krad.service.DocumentDictionaryService;
054import org.kuali.rice.krad.service.DocumentService;
055import org.kuali.rice.krad.service.KRADServiceLocator;
056import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
057import org.kuali.rice.krad.service.MaintenanceDocumentService;
058import org.kuali.rice.krad.util.GlobalVariables;
059import org.kuali.rice.krad.util.KRADConstants;
060import org.kuali.rice.krad.util.NoteType;
061import org.kuali.rice.krad.util.documentserializer.PropertySerializabilityEvaluator;
062import org.w3c.dom.Document;
063import org.w3c.dom.Node;
064import org.w3c.dom.NodeList;
065import org.xml.sax.InputSource;
066import org.xml.sax.SAXException;
067
068import com.thoughtworks.xstream.core.BaseException;
069
070/**
071 * Created by sheiksalahudeenm on 10/14/14.
072 */
073@Entity
074@Table(name = "KRNS_MAINT_DOC_T",uniqueConstraints= {
075        @UniqueConstraint(name="KRNS_MAINT_DOC_TC0",columnNames="OBJ_ID")
076})
077public class MaintenanceDocumentBase extends DocumentBase implements MaintenanceDocument, SessionDocument {
078    protected static final int SUB_OBJECT_MATERIALIZATION_DEPTH = 3;
079    private static final long serialVersionUID = -505085142412593305L;
080    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(MaintenanceDocumentBase.class);
081
082    public static final String MAINTAINABLE_IMPL_CLASS = "maintainableImplClass";
083    public static final String OLD_MAINTAINABLE_TAG_NAME = "oldMaintainableObject";
084    public static final String NEW_MAINTAINABLE_TAG_NAME = "newMaintainableObject";
085    public static final String MAINTENANCE_ACTION_TAG_NAME = "maintenanceAction";
086    public static final String NOTES_TAG_NAME = "notes";
087
088    @Transient
089    private static transient DocumentDictionaryService documentDictionaryService;
090    @Transient
091    private static transient MaintenanceDocumentService maintenanceDocumentService;
092    @Transient
093    private static transient DocumentService documentService;
094
095    @Transient
096    protected Maintainable oldMaintainableObject;
097
098    @Transient
099    protected Maintainable newMaintainableObject;
100
101    @Lob
102    @Column(name = "DOC_CNTNT")
103    protected String xmlDocumentContents;
104    @Transient
105    protected boolean fieldsClearedOnCopy;
106    @Transient
107    protected boolean displayTopicFieldInNotes = false;
108    @Transient
109    protected String attachmentPropertyName;
110    @Transient
111    protected String attachmentListPropertyName;
112    @Transient
113    protected String attachmentCollectionName;
114
115    @OneToOne(fetch = FetchType.LAZY,
116            cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
117    @JoinColumn(name = "DOC_HDR_ID",
118            insertable = false, updatable = false)
119    protected DocumentAttachment attachment;
120
121    @OneToMany(fetch = FetchType.LAZY,
122            cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
123    @JoinColumn(name = "DOC_HDR_ID",
124            insertable = false, updatable = false)
125    protected List<MultiDocumentAttachment> attachments;
126
127    public MaintenanceDocumentBase() {
128        super();
129        fieldsClearedOnCopy = false;
130    }
131
132    /**
133     * Initializies the maintainables.
134     */
135    public MaintenanceDocumentBase(String documentTypeName) {
136        this();
137        Class clazz = getDocumentDictionaryService().getMaintainableClass(documentTypeName);
138        try {
139            oldMaintainableObject = (Maintainable) clazz.newInstance();
140            newMaintainableObject = (Maintainable) clazz.newInstance();
141
142            // initialize maintainable with a data object
143            Class<?> dataObjectClazz = getDocumentDictionaryService().getMaintenanceDataObjectClass(documentTypeName);
144            oldMaintainableObject.setDataObject(dataObjectClazz.newInstance());
145            oldMaintainableObject.setDataObjectClass(dataObjectClazz);
146            newMaintainableObject.setDataObject(dataObjectClazz.newInstance());
147            newMaintainableObject.setDataObjectClass(dataObjectClazz);
148        } catch (InstantiationException e) {
149            LOG.error("Unable to initialize maintainables of type " + clazz.getName());
150            throw new RuntimeException("Unable to initialize maintainables of type " + clazz.getName());
151        } catch (IllegalAccessException e) {
152            LOG.error("Unable to initialize maintainables of type " + clazz.getName());
153            throw new RuntimeException("Unable to initialize maintainables of type " + clazz.getName());
154        }
155    }
156
157    /**
158     * Builds out the document title for maintenance documents
159     *
160     * <p>This will get loaded into the flex doc and passed into
161     * workflow. It will be searchable.
162     * </p>
163     *
164     * @return document title
165     */
166    @Override
167    public String getDocumentTitle() {
168        String documentTitle = "";
169
170        documentTitle = newMaintainableObject.getDocumentTitle(this);
171        if (StringUtils.isNotBlank(documentTitle)) {
172            // if doc title has been overridden by maintainable, use it
173            return documentTitle;
174        }
175
176        // TODO - build out with bo label once we get the data dictionary stuff in place
177        // build out the right classname
178        String className = newMaintainableObject.getDataObject().getClass().getName();
179        String truncatedClassName = className.substring(className.lastIndexOf('.') + 1);
180        if (isOldDataObjectInDocument()) {
181            if (KRADConstants.MAINTENANCE_COPY_ACTION.equals(oldMaintainableObject.getMaintenanceAction())) {
182                documentTitle = "Copy ";
183            } else {
184                documentTitle = "Edit ";
185            }
186        } else {
187            documentTitle = "New ";
188        }
189        documentTitle += truncatedClassName + " - ";
190        documentTitle += this.getDocumentHeader().getDocumentDescription() + " ";
191        return documentTitle;
192    }
193
194    /**
195     * Check if oldMaintainable is specified in the XML of the maintenance document
196     *
197     * @param xmlDocument Maintenance document in XML form
198     * @return true if an oldMainainable exists in the xmlDocument, false otherwise
199     */
200    protected boolean isOldMaintainableInDocument(Document xmlDocument) {
201        boolean isOldMaintainableInExistence = false;
202        if (xmlDocument.getElementsByTagName(OLD_MAINTAINABLE_TAG_NAME).getLength() > 0) {
203            isOldMaintainableInExistence = true;
204        }
205        return isOldMaintainableInExistence;
206    }
207
208    /**
209     * @see org.kuali.rice.krad.maintenance.Maintainable#isOldDataObjectInDocument()
210     */
211    @Override
212    public boolean isOldDataObjectInDocument() {
213        boolean isOldBusinessObjectInExistence = false;
214        if (oldMaintainableObject == null || oldMaintainableObject.getDataObject() == null) {
215            isOldBusinessObjectInExistence = false;
216        } else {
217            isOldBusinessObjectInExistence = oldMaintainableObject.isOldDataObjectInDocument();
218        }
219        return isOldBusinessObjectInExistence;
220    }
221
222    /**
223     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#isNew()
224     */
225    @Override
226    public boolean isNew() {
227        return MaintenanceUtils.isMaintenanceDocumentCreatingNewRecord(newMaintainableObject.getMaintenanceAction());
228    }
229
230    /**
231     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#isEdit()
232     */
233    @Override
234    public boolean isEdit() {
235        if (KRADConstants.MAINTENANCE_EDIT_ACTION.equalsIgnoreCase(newMaintainableObject.getMaintenanceAction())) {
236            return true;
237        } else {
238            return false;
239        }
240    }
241
242    /**
243     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#isNewWithExisting()
244     */
245    @Override
246    public boolean isNewWithExisting() {
247        if (KRADConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equalsIgnoreCase(
248                newMaintainableObject.getMaintenanceAction())) {
249            return true;
250        } else {
251            return false;
252        }
253    }
254
255    /**
256     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#populateMaintainablesFromXmlDocumentContents()
257     */
258    @Override
259    public void populateMaintainablesFromXmlDocumentContents() {
260        // get a hold of the parsed xml document, then read the classname,
261        // then instantiate one to two instances depending on content
262        // then populate those instances
263        if (!StringUtils.isEmpty(xmlDocumentContents)) {
264            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
265            /* KULRICE-12304:
266            * Modified this block to fetch the document type and use that
267            * document type to fetch the maintainable class from the document
268            * dictionary service.  This was necessary since the maintainable
269            * class which is persisted in the document content XML may be out
270            * of date if it changes across version updates.
271            */
272            try {
273                DocumentBuilder builder = factory.newDocumentBuilder();
274                Document xmlDocument = builder.parse(new InputSource(new StringReader(xmlDocumentContents)));
275                String documentTypeName = KewApiServiceLocator.getWorkflowDocumentService().getDocument(this.getDocumentNumber()).getDocumentTypeName();
276                Class<? extends Maintainable> maintainableClass = getDocumentDictionaryService().getMaintainableClass(documentTypeName);
277                if (isOldMaintainableInDocument(xmlDocument)) {
278                    oldMaintainableObject = (Maintainable) maintainableClass.newInstance();
279                    Object dataObject = getDataObjectFromXML(OLD_MAINTAINABLE_TAG_NAME);
280
281                    String oldMaintenanceAction = getMaintenanceAction(xmlDocument, OLD_MAINTAINABLE_TAG_NAME);
282                    oldMaintainableObject.setMaintenanceAction(oldMaintenanceAction);
283
284                    oldMaintainableObject.setDataObject(dataObject);
285                    oldMaintainableObject.setDataObjectClass(dataObject.getClass());
286                }
287                newMaintainableObject = (Maintainable) maintainableClass.newInstance();
288                Object bo = getDataObjectFromXML(NEW_MAINTAINABLE_TAG_NAME);
289                newMaintainableObject.setDataObject(bo);
290                newMaintainableObject.setDataObjectClass(bo.getClass());
291
292                String newMaintenanceAction = getMaintenanceAction(xmlDocument, NEW_MAINTAINABLE_TAG_NAME);
293                newMaintainableObject.setMaintenanceAction(newMaintenanceAction);
294
295                if (newMaintainableObject.isNotesEnabled()) {
296                    List<Note> notes = getNotesFromXml(NOTES_TAG_NAME);
297                    setNotes(notes);
298                }
299            } catch (ParserConfigurationException e) {
300                LOG.error("Error while parsing document contents", e);
301                throw new RuntimeException("Could not load document contents from xml", e);
302            } catch (SAXException e) {
303                LOG.error("Error while parsing document contents", e);
304                throw new RuntimeException("Could not load document contents from xml", e);
305            } catch (IOException e) {
306                LOG.error("Error while parsing document contents", e);
307                throw new RuntimeException("Could not load document contents from xml", e);
308            } catch (InstantiationException e) {
309                LOG.error("Error while parsing document contents", e);
310                throw new RuntimeException("Could not load document contents from xml", e);
311            } catch (IllegalAccessException e) {
312                LOG.error("Error while parsing document contents", e);
313                throw new RuntimeException("Could not load document contents from xml", e);
314            }
315        }
316    }
317
318    /**
319     * This method is a lame containment of ugly DOM walking code. This is ONLY necessary because of the version
320     * conflicts between Xalan.jar in 2.6.x and 2.7. As soon as we can upgrade to 2.7, this will be switched to using
321     * XPath, which is faster and much easier on the eyes.
322     *
323     * @param xmlDocument
324     * @param oldOrNewElementName - String oldMaintainableObject or newMaintainableObject
325     * @return the value of the element, or null if none was there
326     */
327    protected String getMaintenanceAction(Document xmlDocument, String oldOrNewElementName) {
328        if (StringUtils.isBlank(oldOrNewElementName)) {
329            throw new IllegalArgumentException("oldOrNewElementName may not be blank, null, or empty-string.");
330        }
331
332        String maintenanceAction = null;
333        NodeList rootChildren = xmlDocument.getDocumentElement().getChildNodes();
334        for (int i = 0; i < rootChildren.getLength(); i++) {
335            Node rootChild = rootChildren.item(i);
336            if (oldOrNewElementName.equalsIgnoreCase(rootChild.getNodeName())) {
337                NodeList maintChildren = rootChild.getChildNodes();
338                for (int j = 0; j < maintChildren.getLength(); j++) {
339                    Node maintChild = maintChildren.item(j);
340                    if (MAINTENANCE_ACTION_TAG_NAME.equalsIgnoreCase(maintChild.getNodeName())) {
341                        maintenanceAction = maintChild.getChildNodes().item(0).getNodeValue();
342                    }
343                }
344            }
345        }
346        return maintenanceAction;
347    }
348
349    /**
350     * Get notes from XML
351     *
352     * @param notesTagName the xml tag name of the notes
353     * @return list of <code>Note</code>s
354     */
355    private List<Note> getNotesFromXml(String notesTagName) {
356        String notesXml = StringUtils.substringBetween(xmlDocumentContents, "<" + notesTagName + ">",
357                "</" + notesTagName + ">");
358        if (StringUtils.isBlank(notesXml)) {
359            return Collections.emptyList();
360        }
361        List<Note> notes = (List<Note>) KRADServiceLocator.getXmlObjectSerializerService().fromXml(notesXml);
362        if (notes == null) {
363            return Collections.emptyList();
364        }
365        return notes;
366    }
367
368    /**
369     * Get data object from XML
370     *
371     * <p>
372     * Retrieves substring of document contents from maintainable tag name. Then use xml service to translate xml into
373     * a business object.
374     * </p>
375     *
376     * @param maintainableTagName the xml tag name of the maintainable
377     * @return data object
378     */
379    protected Object getDataObjectFromXML(String maintainableTagName) {
380        String maintXml = StringUtils.substringBetween(xmlDocumentContents, "<" + maintainableTagName + ">",
381                "</" + maintainableTagName + ">");
382        /*KULRICE-12304*/
383        try {
384            boolean ignoreMissingFields = false;
385            String classAndDocTypeNames = ConfigContext.getCurrentContextConfig().getProperty(KRADConstants.Config.IGNORE_MISSIONG_FIELDS_ON_DESERIALIZE);
386            if (!StringUtils.isEmpty(classAndDocTypeNames)) {
387                String classNameOnXML = StringUtils.substringBetween(xmlDocumentContents, "<" + maintainableTagName + "><", ">");
388                String classNamesNoSpaces = removeSpacesAround(classAndDocTypeNames);
389                List<String> classAndDocTypeNamesList = Arrays.asList(org.apache.commons.lang.StringUtils.split(classNamesNoSpaces, ","));
390                String originalDocTypeId = getDocumentHeader().getWorkflowDocument().getDocumentTypeId();
391                DocumentType docType = KewApiServiceLocator.getDocumentTypeService().getDocumentTypeById(originalDocTypeId);
392
393                while (docType != null && !ignoreMissingFields) {
394                    for(String classNameOrDocTypeName : classAndDocTypeNamesList){
395                        if (docType.getName().equalsIgnoreCase(classNameOrDocTypeName) ||
396                                classNameOnXML.equalsIgnoreCase(classNameOrDocTypeName)) {
397                            ignoreMissingFields = true;
398                            break;
399                        }
400                    }
401                    if (!StringUtils.isEmpty(docType.getParentId())) {
402                        docType = KewApiServiceLocator.getDocumentTypeService().getDocumentTypeById(docType.getParentId());
403                    } else {
404                        docType = null;
405                    }
406                }
407            }
408            if (!ignoreMissingFields) {
409                return KRADServiceLocator.getXmlObjectSerializerService().fromXml(maintXml);
410            } else {
411                return KRADServiceLocator.getXmlObjectSerializerIgnoreMissingFieldsService().fromXml(maintXml);
412            }
413        }catch (BaseException e) {
414            String convertedXml = KRADServiceLocatorWeb.getMaintainableXMLConversionService().transformMaintainableXML(maintXml);
415            return KRADServiceLocator.getXmlObjectSerializerService().fromXml(convertedXml);
416        }/*KULRICE-12304*/
417    }
418
419    /**
420     * Removes the spaces around the elements on a csv list of elements.
421     * <p>
422     * A null input will return a null output.
423     * </p>
424     *
425     * @param csv a list of elements in csv format e.g. foo, bar, baz
426     * @return a list of elements in csv format without spaces e.g. foo,bar,baz
427     */
428    private String removeSpacesAround(String csv) {
429        if (csv == null) {
430            return null;
431        }
432
433        final StringBuilder result = new StringBuilder();
434        for (final String value : csv.split(",")) {
435            if (!"".equals(value.trim())) {
436                result.append(value.trim());
437                result.append(",");
438            }
439        }
440
441        //remove trailing comma
442        int i = result.lastIndexOf(",");
443        if (i != -1) {
444            result.deleteCharAt(i);
445        }
446
447        return result.toString();
448    }
449
450    /**
451     * Populates the xml document contents from the maintainables.
452     *
453     * @see MaintenanceDocument#populateXmlDocumentContentsFromMaintainables()
454     */
455    @Override
456    public void populateXmlDocumentContentsFromMaintainables() {
457        StringBuilder docContentBuffer = new StringBuilder();
458        docContentBuffer.append("<maintainableDocumentContents maintainableImplClass=\"").append(
459                newMaintainableObject.getClass().getName()).append("\">");
460
461        // if business objects notes are enabled then we need to persist notes to the XML
462        if (getNewMaintainableObject().isNotesEnabled()) {
463            docContentBuffer.append("<" + NOTES_TAG_NAME + ">");
464            // copy notes to a non-ojb Proxied ArrayList to get rid of the usage of those proxies
465            // note: XmlObjectSerializerServiceImpl should be doing this for us but it does not
466            // appear to be working (at least in this case) and the xml comes through
467            // with the fully qualified ListProxyDefault class name from OJB embedded inside it.
468            List<Note> noteList = new ArrayList<Note>();
469            for (Note note : getNotes()) {
470                noteList.add(note);
471            }
472            docContentBuffer.append(KRADServiceLocator.getXmlObjectSerializerService().toXml(noteList));
473            docContentBuffer.append("</" + NOTES_TAG_NAME + ">");
474        }
475        if (oldMaintainableObject != null && oldMaintainableObject.getDataObject() != null) {
476            // TODO: refactor this out into a method
477            docContentBuffer.append("<" + OLD_MAINTAINABLE_TAG_NAME + ">");
478
479            Object oldBo = oldMaintainableObject.getDataObject();
480
481            // hack to resolve XStream not dealing well with Proxies
482            //KradDataServiceLocator.getDataObjectService().wrap(oldBo).materializeReferencedObjectsToDepth(SUB_OBJECT_MATERIALIZATION_DEPTH);
483            KRADServiceLocatorWeb.getLegacyDataAdapter().materializeAllSubObjects(oldBo);
484
485            docContentBuffer.append(getBusinessObjectSerializerService().serializeBusinessObjectToXml(oldBo));
486
487            // add the maintainable's maintenanceAction
488            docContentBuffer.append("<" + MAINTENANCE_ACTION_TAG_NAME + ">");
489            docContentBuffer.append(oldMaintainableObject.getMaintenanceAction());
490            docContentBuffer.append("</" + MAINTENANCE_ACTION_TAG_NAME + ">\n");
491
492            docContentBuffer.append("</" + OLD_MAINTAINABLE_TAG_NAME + ">");
493        }
494        docContentBuffer.append("<" + NEW_MAINTAINABLE_TAG_NAME + ">");
495
496        Object newBo = newMaintainableObject.getDataObject();
497
498        //KradDataServiceLocator.getDataObjectService().wrap(newBo).materializeReferencedObjectsToDepth(SUB_OBJECT_MATERIALIZATION_DEPTH);
499        KRADServiceLocatorWeb.getLegacyDataAdapter().materializeAllSubObjects(newBo);
500
501        docContentBuffer.append(getBusinessObjectSerializerService().serializeBusinessObjectToXml(newBo));
502
503        // add the maintainable's maintenanceAction
504        docContentBuffer.append("<" + MAINTENANCE_ACTION_TAG_NAME + ">");
505        docContentBuffer.append(newMaintainableObject.getMaintenanceAction());
506        docContentBuffer.append("</" + MAINTENANCE_ACTION_TAG_NAME + ">\n");
507
508        docContentBuffer.append("</" + NEW_MAINTAINABLE_TAG_NAME + ">");
509        docContentBuffer.append("</maintainableDocumentContents>");
510        xmlDocumentContents = docContentBuffer.toString();
511    }
512
513    /**
514     * @see org.kuali.rice.krad.document.DocumentBase#doRouteStatusChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange)
515     */
516    @Override
517    public void doRouteStatusChange(DocumentRouteStatusChange statusChangeEvent) {
518        super.doRouteStatusChange(statusChangeEvent);
519
520        WorkflowDocument workflowDocument = getDocumentHeader().getWorkflowDocument();
521        getNewMaintainableObject().doRouteStatusChange(getDocumentHeader());
522        // commit the changes to the Maintainable BusinessObject when it goes to Processed (ie, fully approved),
523        // and also unlock it
524        if (workflowDocument.isProcessed()) {
525            final String documentNumber = getDocumentHeader().getDocumentNumber();
526            newMaintainableObject.setDocumentNumber(documentNumber);
527
528            //Populate Attachment Property
529            if (newMaintainableObject.getDataObject() instanceof PersistableAttachment) {
530                populateAttachmentBeforeSave();
531            }
532
533            //Populate Attachment Property
534            if (newMaintainableObject.getDataObject() instanceof PersistableAttachmentList) {
535                populateBoAttachmentListBeforeSave();
536            }
537
538            newMaintainableObject.saveDataObject();
539
540            if (!getDocumentService().saveDocumentNotes(this)) {
541                throw new IllegalStateException(
542                        "Failed to save document notes, this means that the note target was not ready for notes to be attached when it should have been.");
543            }
544
545            //Attachment should be deleted from Maintenance Document attachment table
546            deleteDocumentAttachment();
547            deleteDocumentAttachmentList();
548
549            getMaintenanceDocumentService().deleteLocks(documentNumber);
550
551            //for issue 3070, check if delete record
552            if (this.checkAllowsRecordDeletion() && this.checkMaintenanceAction() &&
553                    this.checkDeletePermission(newMaintainableObject.getDataObject())) {
554                newMaintainableObject.deleteDataObject();
555            }
556        }
557
558        // unlock the document when its canceled or disapproved or placed inException status
559        if (workflowDocument.isCanceled() || workflowDocument.isDisapproved() || workflowDocument.isRecalled() || workflowDocument.isException()) {
560            //Attachment should be deleted from Maintenance Document attachment table
561            deleteDocumentAttachment();
562            deleteDocumentAttachmentList();
563
564            String documentNumber = getDocumentHeader().getDocumentNumber();
565            getMaintenanceDocumentService().deleteLocks(documentNumber);
566        }
567    }
568
569    /**
570     * @see org.kuali.rice.krad.document.DocumentBase#getWorkflowEngineDocumentIdsToLock()
571     */
572    @Override
573    public List<String> getWorkflowEngineDocumentIdsToLock() {
574        if (newMaintainableObject != null) {
575            return newMaintainableObject.getWorkflowEngineDocumentIdsToLock();
576        }
577        return Collections.emptyList();
578    }
579
580    /**
581     * @see org.kuali.rice.krad.document.Document#prepareForSave()
582     */
583    @Override
584    public void prepareForSave() {
585        if (newMaintainableObject != null) {
586            newMaintainableObject.prepareForSave();
587        }
588    }
589
590    /**
591     * @see org.kuali.rice.krad.document.DocumentBase#processAfterRetrieve()
592     */
593    @Override
594    public void processAfterRetrieve() {
595
596        super.processAfterRetrieve();
597
598        populateMaintainablesFromXmlDocumentContents();
599        if (oldMaintainableObject != null) {
600            oldMaintainableObject.setDocumentNumber(documentNumber);
601        }
602        if (newMaintainableObject != null) {
603            newMaintainableObject.setDocumentNumber(documentNumber);
604            newMaintainableObject.processAfterRetrieve();
605            if (newMaintainableObject.getDataObject() instanceof PersistableAttachment) {
606                populateAttachmentForBO();
607            }
608            if (newMaintainableObject.getDataObject() instanceof PersistableAttachmentList) {
609                populateAttachmentListForBO();
610            }
611            // If a maintenance lock exists, warn the user.
612            checkForLockingDocument(false);
613        }
614    }
615
616    /**
617     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#getNewMaintainableObject()
618     */
619    @Override
620    public Maintainable getNewMaintainableObject() {
621        return newMaintainableObject;
622    }
623
624    /**
625     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#setNewMaintainableObject(Maintainable)
626     */
627    @Override
628    public void setNewMaintainableObject(Maintainable newMaintainableObject) {
629        this.newMaintainableObject = newMaintainableObject;
630    }
631
632    /**
633     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#getOldMaintainableObject()
634     */
635    @Override
636    public Maintainable getOldMaintainableObject() {
637        return oldMaintainableObject;
638    }
639
640    /**
641     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#setOldMaintainableObject(Maintainable)
642     */
643    @Override
644    public void setOldMaintainableObject(Maintainable oldMaintainableObject) {
645        this.oldMaintainableObject = oldMaintainableObject;
646    }
647
648    /**
649     * @see org.kuali.rice.krad.document.DocumentBase#setDocumentNumber(java.lang.String)
650     */
651    @Override
652    public void setDocumentNumber(String documentNumber) {
653        super.setDocumentNumber(documentNumber);
654
655        // set the finDocNumber on the Maintainable
656        oldMaintainableObject.setDocumentNumber(documentNumber);
657        newMaintainableObject.setDocumentNumber(documentNumber);
658    }
659
660    /**
661     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#isFieldsClearedOnCopy()
662     */
663    @Override
664    public final boolean isFieldsClearedOnCopy() {
665        return fieldsClearedOnCopy;
666    }
667
668    /**
669     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#setFieldsClearedOnCopy(boolean)
670     */
671    @Override
672    public final void setFieldsClearedOnCopy(boolean fieldsClearedOnCopy) {
673        this.fieldsClearedOnCopy = fieldsClearedOnCopy;
674    }
675
676    /**
677     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#getXmlDocumentContents()
678     */
679    @Override
680    public String getXmlDocumentContents() {
681        return xmlDocumentContents;
682    }
683
684    /**
685     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#setXmlDocumentContents(String)
686     */
687    @Override
688    public void setXmlDocumentContents(String xmlDocumentContents) {
689        this.xmlDocumentContents = xmlDocumentContents;
690    }
691
692    /**
693     * @see org.kuali.rice.krad.document.Document#getAllowsCopy()
694     */
695    @Override
696    public boolean getAllowsCopy() {
697        return getDocumentDictionaryService().getAllowsCopy(this);
698    }
699
700    /**
701     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#isDisplayTopicFieldInNotes()
702     */
703    @Override
704    public boolean isDisplayTopicFieldInNotes() {
705        return displayTopicFieldInNotes;
706    }
707
708    /**
709     * @see MaintenanceDocument#setDisplayTopicFieldInNotes(boolean)
710     */
711    @Override
712    public void setDisplayTopicFieldInNotes(boolean displayTopicFieldInNotes) {
713        this.displayTopicFieldInNotes = displayTopicFieldInNotes;
714    }
715
716    /**
717     * Overridden to avoid serializing the xml twice, because of the xmlDocumentContents property of this object
718     */
719    @Override
720    public String serializeDocumentToXml() {
721        String tempXmlDocumentContents = xmlDocumentContents;
722        xmlDocumentContents = null;
723        String xmlForWorkflow = super.serializeDocumentToXml();
724        xmlDocumentContents = tempXmlDocumentContents;
725        return xmlForWorkflow;
726    }
727
728    /**
729     * @see DocumentBase#prepareForSave(org.kuali.rice.krad.rules.rule.event.DocumentEvent)
730     */
731    @Override
732    public void prepareForSave(DocumentEvent event) {
733        super.prepareForSave(event);
734        if (newMaintainableObject.getDataObject() instanceof PersistableAttachment) {
735            populateDocumentAttachment();
736            populateAttachmentForBO();
737            //clear out attachment file for old data object so it isn't serialized in doc content
738            if (oldMaintainableObject.getDataObject() instanceof PersistableAttachment) {
739                ((PersistableAttachment) oldMaintainableObject.getDataObject()).setAttachmentContent(null);
740            }
741        }
742        if (newMaintainableObject.getDataObject() instanceof PersistableAttachmentList) {
743            populateDocumentAttachmentList();
744            populateAttachmentListForBO();
745            if (oldMaintainableObject.getDataObject() instanceof PersistableAttachmentList) {
746                for (PersistableAttachment pa : ((PersistableAttachmentList<PersistableAttachment>) oldMaintainableObject
747                        .getDataObject()).getAttachments()) {
748                    pa.setAttachmentContent(null);
749                }
750            }
751        }
752        populateXmlDocumentContentsFromMaintainables();
753    }
754
755    /**
756     * The attachment BO is proxied in OJB.  For some reason when an attachment does not yet exist,
757     * refreshReferenceObject is not returning null and the proxy cannot be materialized. So, this method exists to
758     * properly handle the proxied attachment BO.  This is a hack and should be removed post JPA migration.
759     */
760    @Deprecated
761    protected void refreshAttachment() {
762        if (attachment == null) {
763            KradDataServiceLocator.getDataObjectService().wrap(this).fetchRelationship("attachment");
764        }
765    }
766
767    @Deprecated
768    protected void refreshAttachmentList() {
769        if (attachments == null) {
770            KradDataServiceLocator.getDataObjectService().wrap(this).fetchRelationship("attachments");
771        }
772    }
773
774    @Deprecated
775    public void populateAttachmentForBO() { }
776
777    @Deprecated
778    public void populateDocumentAttachment() { }
779
780    @Deprecated
781    public void populateAttachmentListForBO() { }
782
783    @Deprecated
784    public void populateAttachmentBeforeSave() { }
785
786    @Deprecated
787    public void populateDocumentAttachmentList() { }
788
789    @Deprecated
790    public void populateBoAttachmentListBeforeSave() { }
791
792    @Deprecated
793    public void deleteDocumentAttachment() {
794        if ( attachment != null ) {
795            KRADServiceLocatorWeb.getLegacyDataAdapter().delete(attachment);
796            attachment = null;
797        }
798    }
799
800    @Deprecated
801    public void deleteDocumentAttachmentList() {
802        if (CollectionUtils.isNotEmpty(attachments)) {
803            for (MultiDocumentAttachment attachment : attachments) {
804                KRADServiceLocatorWeb.getLegacyDataAdapter().delete(attachment);
805            }
806            attachments = null;
807        }
808    }
809
810    /**
811     * Explicitly NOT calling super here.  This is a complete override of the validation rules behavior.
812     *
813     * @see org.kuali.rice.krad.document.DocumentBase#validateBusinessRules(org.kuali.rice.krad.rules.rule.event.DocumentEvent)
814     */
815    @Override
816    public void validateBusinessRules(DocumentEvent event) {
817        if (GlobalVariables.getMessageMap().hasErrors()) {
818            logErrors();
819            throw new ValidationException("errors occured before business rule");
820        }
821
822        // check for locking documents for MaintenanceDocuments
823        checkForLockingDocument(true);
824
825        // Make sure the business object's version number matches that of the databases copy.
826
827        if (newMaintainableObject != null) {
828            KRADServiceLocatorWeb.getLegacyDataAdapter().verifyVersionNumber(newMaintainableObject.getDataObject());
829        }
830
831        // perform validation against rules engine
832        if (LOG.isInfoEnabled()) {
833            LOG.info("invoking rules engine on document " + getDocumentNumber());
834        }
835
836        boolean isValid = true;
837        isValid = KRADServiceLocatorWeb.getKualiRuleService().applyRules(event);
838
839        // check to see if the br eval passed or failed
840        if (!isValid) {
841            logErrors();
842            // TODO: better error handling at the lower level and a better error message are
843            // needed here
844            throw new ValidationException("business rule evaluation failed");
845        } else if (GlobalVariables.getMessageMap().hasErrors()) {
846            logErrors();
847            if (event instanceof SaveDocumentEvent) {
848                // for maintenance documents, we want to always actually do a save if the
849                // user requests a save, even if there are validation or business rules
850                // failures. this empty if does this, and allows the document to be saved,
851                // even if there are failures.
852                // BR or validation failures on a ROUTE even should always stop the route,
853                // that has not changed
854            } else {
855                throw new ValidationException(
856                        "Unreported errors occurred during business rule evaluation (rule developer needs to put meaningful error messages into global ErrorMap)");
857            }
858        }
859
860        LOG.debug("validation completed");
861    }
862
863    protected void checkForLockingDocument(boolean throwExceptionIfLocked) {
864        MaintenanceUtils.checkForLockingDocument(this, throwExceptionIfLocked);
865    }
866
867    /**
868     * this needs to happen after the document itself is saved, to preserve consistency of the ver_nbr and in the case
869     * of initial save, because this can't be saved until the document is saved initially
870     *
871     * @see org.kuali.rice.krad.document.DocumentBase#postProcessSave(org.kuali.rice.krad.rules.rule.event.DocumentEvent)
872     */
873    @Override
874    public void postProcessSave(DocumentEvent event) {
875        //currently only global documents could change the list of what they're affecting during routing,
876        //so could restrict this to only happening with them, but who knows if that will change, so safest
877        //to always do the delete and re-add...seems a bit inefficient though if nothing has changed, which is
878        //most of the time...could also try to only add/update/delete what's changed, but this is easier
879        if (!(event instanceof SaveDocumentEvent)) { //don't lock until they route
880            getMaintenanceDocumentService().deleteLocks(MaintenanceDocumentBase.this.getDocumentNumber());
881            getMaintenanceDocumentService().storeLocks(MaintenanceDocumentBase.this.getNewMaintainableObject().generateMaintenanceLocks());
882        }
883    }
884
885    /**
886     * @see org.kuali.rice.krad.maintenance.MaintenanceDocument#getDocumentDataObject()
887     */
888    @Override
889    public Object getDocumentDataObject() {
890        return getNewMaintainableObject().getDataObject();
891    }
892
893    /**
894     * <p>The Note target for maintenance documents is determined by whether or not the underlying {@link Maintainable}
895     * supports business object notes or not.  This is determined via a call to {@link
896     * org.kuali.rice.krad.maintenance.Maintainable#isNotesEnabled()}.
897     * The note target is then derived as follows: <p/> <ul> <li>If the {@link Maintainable} supports business object
898     * notes, delegate to {@link #getDocumentDataObject()}. <li>Otherwise, delegate to the default implementation of
899     * getNoteTarget on the superclass which will effectively return a reference to the {@link DocumentHeader}. </ul>
900     *
901     * @see org.kuali.rice.krad.document.Document#getNoteTarget()
902     */
903    @Override
904    public GloballyUnique getNoteTarget() {
905        if (getNewMaintainableObject() == null) {
906            throw new IllegalStateException(
907                    "Failed to acquire the note target.  The new maintainable object on this document is null.");
908        }
909        if (getNewMaintainableObject().isNotesEnabled() && getDocumentDataObject() instanceof GloballyUnique ) {
910            return (GloballyUnique) getDocumentDataObject();
911        }
912        return super.getNoteTarget();
913    }
914
915    /**
916     * The {@link org.kuali.rice.krad.util.NoteType} for maintenance documents is determined by whether or not the underlying {@link
917     * Maintainable} supports business object notes or not.  This is determined via a call to {@link
918     * Maintainable#isNotesEnabled()}.  The {@link org.kuali.rice.krad.util.NoteType} is then derived as follows: <p/> <ul> <li>If the
919     * {@link
920     * Maintainable} supports business object notes, return {@link org.kuali.rice.krad.util.NoteType#BUSINESS_OBJECT}. <li>Otherwise, delegate
921     * to
922     * {@link DocumentBase#getNoteType()} </ul>
923     *
924     * @see org.kuali.rice.krad.document.Document#getNoteType()
925     * @see org.kuali.rice.krad.document.Document#getNoteTarget()
926     */
927    @Override
928    public org.kuali.rice.krad.util.NoteType getNoteType() {
929        if (getNewMaintainableObject().isNotesEnabled()) {
930            return org.kuali.rice.krad.util.NoteType.BUSINESS_OBJECT;
931        }
932        return super.getNoteType();
933    }
934
935    @Override
936    public PropertySerializabilityEvaluator getDocumentPropertySerizabilityEvaluator() {
937        String docTypeName = "";
938        if (newMaintainableObject != null) {
939            docTypeName = getDocumentDictionaryService().getMaintenanceDocumentTypeName(
940                    this.newMaintainableObject.getDataObjectClass());
941        } else { // I don't know why we aren't just using the header in the first place
942            // but, in the case where we can't get it in the way above, attempt to get
943            // it off the workflow document header
944            if (getDocumentHeader() != null && getDocumentHeader().getWorkflowDocument() != null) {
945                docTypeName = getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
946            }
947        }
948        if (!StringUtils.isBlank(docTypeName)) {
949            DocumentEntry documentEntry = getDocumentDictionaryService().getMaintenanceDocumentEntry(docTypeName);
950            if (documentEntry != null) {
951                WorkflowProperties workflowProperties = documentEntry.getWorkflowProperties();
952                WorkflowAttributes workflowAttributes = documentEntry.getWorkflowAttributes();
953                return createPropertySerializabilityEvaluator(workflowProperties, workflowAttributes);
954            } else {
955                LOG.error("Unable to obtain DD DocumentEntry for document type: '" + docTypeName + "'");
956            }
957        } else {
958            LOG.error("Unable to obtain document type name for this document: " + this);
959        }
960        LOG.error("Returning null for the PropertySerializabilityEvaluator");
961        return null;
962    }
963
964    public DocumentAttachment getAttachment() {
965        return this.attachment;
966    }
967
968    public void setAttachment(DocumentAttachment attachment) {
969        this.attachment = attachment;
970    }
971
972    public List<MultiDocumentAttachment> getAttachments() {
973        return this.attachments;
974    }
975
976    public void setAttachments(List<MultiDocumentAttachment> attachments) {
977        this.attachments = attachments;
978    }
979
980    public String getAttachmentPropertyName() {
981        return this.attachmentPropertyName;
982    }
983
984    public void setAttachmentPropertyName(String attachmentPropertyName) {
985        this.attachmentPropertyName = attachmentPropertyName;
986    }
987
988    public String getAttachmentListPropertyName() {
989        return this.attachmentListPropertyName;
990    }
991
992    public void setAttachmentListPropertyName(String attachmentListPropertyName) {
993        this.attachmentListPropertyName = attachmentListPropertyName;
994    }
995
996    public String getAttachmentCollectionName() {
997        return this.attachmentCollectionName;
998    }
999
1000    public void setAttachmentCollectionName(String attachmentCollectionName) {
1001        this.attachmentCollectionName = attachmentCollectionName;
1002    }
1003
1004    /**
1005     * This method to check whether the document class implements SessionDocument
1006     *
1007     * TODO: move to KNS maintenance document base
1008     *
1009     * @return true if the document is a session document
1010     */
1011    public boolean isSessionDocument() {
1012        return SessionDocument.class.isAssignableFrom(this.getClass());
1013    }
1014
1015    /**
1016     * Returns whether or not the new maintainable object supports custom lock descriptors. Will always return false if
1017     * the new maintainable is null.
1018     *
1019     * @see org.kuali.rice.krad.document.Document#useCustomLockDescriptors()
1020     * @see org.kuali.rice.krad.maintenance.Maintainable#useCustomLockDescriptors()
1021     */
1022    @Override
1023    public boolean useCustomLockDescriptors() {
1024        return (newMaintainableObject != null && newMaintainableObject.useCustomLockDescriptors());
1025    }
1026
1027    /**
1028     * Returns the custom lock descriptor generated by the new maintainable object, if defined. Will throw a
1029     * PessimisticLockingException if the new maintainable is null.
1030     *
1031     * @see org.kuali.rice.krad.document.Document#getCustomLockDescriptor(org.kuali.rice.kim.api.identity.Person)
1032     * @see org.kuali.rice.krad.maintenance.Maintainable#getCustomLockDescriptor(org.kuali.rice.kim.api.identity.Person)
1033     */
1034    @Override
1035    public String getCustomLockDescriptor(Person user) {
1036        if (newMaintainableObject == null) {
1037            throw new PessimisticLockingException("Maintenance Document " + getDocumentNumber() +
1038                    " is using pessimistic locking with custom lock descriptors, but no new maintainable object has been defined");
1039        }
1040        return newMaintainableObject.getCustomLockDescriptor(user);
1041    }
1042
1043    protected DocumentDictionaryService getDocumentDictionaryService() {
1044        if (documentDictionaryService == null) {
1045            documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1046        }
1047        return documentDictionaryService;
1048    }
1049
1050    protected MaintenanceDocumentService getMaintenanceDocumentService() {
1051        if (maintenanceDocumentService == null) {
1052            maintenanceDocumentService = KRADServiceLocatorWeb.getMaintenanceDocumentService();
1053        }
1054        return maintenanceDocumentService;
1055    }
1056
1057    protected DocumentService getDocumentService() {
1058        if (documentService == null) {
1059            documentService = KRADServiceLocatorWeb.getDocumentService();
1060        }
1061        return documentService;
1062    }
1063
1064    /**
1065     * @return the service used for serializing maintained business / data objects
1066     */
1067    protected BusinessObjectSerializerService getBusinessObjectSerializerService() {
1068        return KRADServiceLocator.getDataObjectSerializerService();
1069    }
1070
1071    //for issue KULRice3070
1072    protected boolean checkAllowsRecordDeletion() {
1073        Boolean allowsRecordDeletion = KRADServiceLocatorWeb.getDocumentDictionaryService().getAllowsRecordDeletion(
1074                this.getNewMaintainableObject().getDataObjectClass());
1075        if (allowsRecordDeletion != null) {
1076            return allowsRecordDeletion.booleanValue();
1077        } else {
1078            return false;
1079        }
1080    }
1081
1082    //for KULRice3070
1083    protected boolean checkMaintenanceAction() {
1084        return this.getNewMaintainableObject().getMaintenanceAction().equals(KRADConstants.MAINTENANCE_DELETE_ACTION);
1085    }
1086
1087    //for KULRice3070
1088    protected boolean checkDeletePermission(Object dataObject) {
1089        boolean allowsMaintain = false;
1090
1091        String maintDocTypeName = KRADServiceLocatorWeb.getDocumentDictionaryService().getMaintenanceDocumentTypeName(
1092                dataObject.getClass());
1093
1094        if (StringUtils.isNotBlank(maintDocTypeName)) {
1095            allowsMaintain = KRADServiceLocatorWeb.getDataObjectAuthorizationService().canMaintain(dataObject,
1096                    GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
1097        }
1098        return allowsMaintain;
1099    }
1100
1101   /* *//**
1102     * Save the KRAD document header via the document header service.
1103     *//*
1104    @Override
1105    @PrePersist
1106    protected void prePersist() {
1107        super.prePersist();
1108        // KRAD/JPA - have to change the handle to object to that just saved
1109
1110        *//* Modified by 'Sheik Salahudeen' for fixing the 'workflow document is null' issue in KRAD Transaction document with JPA.
1111        * Removed the code for saving document header. Because document header will be stored by document service.*//*
1112
1113        // documentHeader = KRADServiceLocatorWeb.getDocumentHeaderService().saveDocumentHeader(documentHeader);
1114    }*/
1115
1116    /**
1117     * Copied this method from FinancialSystemMaintenanceDocument.java
1118     */
1119    /**
1120     * This method is used for routing and simply returns the initiator's Chart code.
1121     * @return The Chart code of the document initiator
1122     */
1123    public String getInitiatorChartOfAccountsCode() {
1124        String[] chartOrg = getInitiatorPrimaryDepartmentCode();
1125        return chartOrg[0];
1126    }
1127
1128    protected String[] getInitiatorPrimaryDepartmentCode() {
1129
1130        String netID = documentHeader.getWorkflowDocument().getInitiatorPrincipalId();
1131        Person person =  KimApiServiceLocator.getPersonService().getPerson(netID);
1132
1133        String deptCode = person.getPrimaryDepartmentCode();
1134        String[] chartOrg = deptCode.split("-");
1135        return chartOrg;
1136
1137    }
1138
1139    /**
1140     * This method is used for routing and simply returns the initiator's Organization code.
1141     * @return The Organization code of the document initiator
1142     */
1143    public String getInitiatorOrganizationCode() {
1144        String[] chartOrg = getInitiatorPrimaryDepartmentCode();
1145        return chartOrg[1];
1146    }
1147
1148}