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