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