View Javadoc

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