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