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