001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.maintenance; 017 018import java.io.IOException; 019import java.io.StringReader; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collections; 023import java.util.List; 024 025import javax.persistence.CascadeType; 026import javax.persistence.Column; 027import javax.persistence.Entity; 028import javax.persistence.FetchType; 029import javax.persistence.JoinColumn; 030import javax.persistence.Lob; 031import javax.persistence.ManyToOne; 032import javax.persistence.OneToMany; 033import javax.persistence.OneToOne; 034import javax.persistence.Table; 035import javax.persistence.Transient; 036import javax.persistence.UniqueConstraint; 037import javax.xml.parsers.DocumentBuilder; 038import javax.xml.parsers.DocumentBuilderFactory; 039import javax.xml.parsers.ParserConfigurationException; 040 041import org.apache.commons.collections.CollectionUtils; 042import org.apache.commons.lang.StringUtils; 043import org.kuali.rice.core.api.config.property.ConfigContext; 044import org.kuali.rice.core.api.mo.common.GloballyUnique; 045import org.kuali.rice.kew.api.KewApiServiceLocator; 046import org.kuali.rice.kew.api.WorkflowDocument; 047import org.kuali.rice.kew.api.doctype.DocumentType; 048import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 049import org.kuali.rice.kim.api.identity.Person; 050import org.kuali.rice.krad.bo.DocumentAttachment; 051import org.kuali.rice.krad.bo.DocumentHeader; 052import org.kuali.rice.krad.bo.MultiDocumentAttachment; 053import org.kuali.rice.krad.bo.Note; 054import org.kuali.rice.krad.bo.PersistableAttachment; 055import org.kuali.rice.krad.bo.PersistableAttachmentList; 056import org.kuali.rice.krad.data.KradDataServiceLocator; 057import org.kuali.rice.krad.datadictionary.DocumentEntry; 058import org.kuali.rice.krad.datadictionary.WorkflowAttributes; 059import org.kuali.rice.krad.datadictionary.WorkflowProperties; 060import org.kuali.rice.krad.document.DocumentBase; 061import org.kuali.rice.krad.document.SessionDocument; 062import org.kuali.rice.krad.exception.PessimisticLockingException; 063import org.kuali.rice.krad.exception.ValidationException; 064import org.kuali.rice.krad.rules.rule.event.DocumentEvent; 065import org.kuali.rice.krad.rules.rule.event.SaveDocumentEvent; 066import org.kuali.rice.krad.service.BusinessObjectSerializerService; 067import org.kuali.rice.krad.service.DocumentDictionaryService; 068import org.kuali.rice.krad.service.DocumentService; 069import org.kuali.rice.krad.service.KRADServiceLocator; 070import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 071import org.kuali.rice.krad.service.MaintenanceDocumentService; 072import org.kuali.rice.krad.util.GlobalVariables; 073import org.kuali.rice.krad.util.KRADConstants; 074import org.kuali.rice.krad.util.NoteType; 075import org.kuali.rice.krad.util.documentserializer.PropertySerializabilityEvaluator; 076import org.w3c.dom.Document; 077import org.w3c.dom.Node; 078import org.w3c.dom.NodeList; 079import org.xml.sax.InputSource; 080import org.xml.sax.SAXException; 081 082import com.thoughtworks.xstream.core.BaseException; 083 084/** 085 * Document class for all maintenance documents which wraps the maintenance object in 086 * a <code>Maintainable</code> that is also used for various callbacks 087 * 088 * <p> 089 * The maintenance xml structure will be: {@code <maintainableDocumentContents maintainableImplClass="className"> 090 * <oldMaintainableObject>... </oldMaintainableObject> <newMaintainableObject>... </newMaintainableObject> 091 * </maintainableDocumentContents> Maintenance Document} 092 * </p> 093 * 094 * @author Kuali Rice Team (rice.collab@kuali.org) 095 */ 096@Entity 097@Table(name = "KRNS_MAINT_DOC_T",uniqueConstraints= { 098 @UniqueConstraint(name="KRNS_MAINT_DOC_TC0",columnNames="OBJ_ID") 099}) 100public 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}