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