001 /** 002 * Copyright 2005-2014 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 */ 016 package org.kuali.rice.krad.service.impl; 017 018 import java.io.Serializable; 019 import java.util.ArrayList; 020 import java.util.Arrays; 021 import java.util.List; 022 import java.util.Map; 023 024 import org.apache.commons.lang.StringUtils; 025 import org.apache.log4j.Logger; 026 import org.kuali.rice.core.api.criteria.Predicate; 027 import org.kuali.rice.core.api.criteria.PredicateFactory; 028 import org.kuali.rice.core.api.criteria.QueryByCriteria; 029 import org.kuali.rice.core.api.util.RiceKeyConstants; 030 import org.kuali.rice.core.api.util.io.SerializationUtils; 031 import org.kuali.rice.core.framework.persistence.jta.TransactionalNoValidationExceptionRollback; 032 import org.kuali.rice.kew.api.exception.WorkflowException; 033 import org.kuali.rice.krad.bo.DataObjectBase; 034 import org.kuali.rice.krad.bo.PersistableBusinessObject; 035 import org.kuali.rice.krad.data.DataObjectService; 036 import org.kuali.rice.krad.data.KradDataServiceLocator; 037 import org.kuali.rice.krad.exception.DocumentTypeAuthorizationException; 038 import org.kuali.rice.krad.maintenance.Maintainable; 039 import org.kuali.rice.krad.maintenance.MaintenanceDocument; 040 import org.kuali.rice.krad.maintenance.MaintenanceLock; 041 import org.kuali.rice.krad.service.DataObjectAuthorizationService; 042 import org.kuali.rice.krad.service.DocumentDictionaryService; 043 import org.kuali.rice.krad.service.DocumentService; 044 import org.kuali.rice.krad.service.LegacyDataAdapter; 045 import org.kuali.rice.krad.service.MaintenanceDocumentService; 046 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 047 import org.kuali.rice.krad.util.GlobalVariables; 048 import org.kuali.rice.krad.util.KRADConstants; 049 import org.kuali.rice.krad.util.KRADPropertyConstants; 050 import org.kuali.rice.krad.util.KRADUtils; 051 import org.springframework.beans.factory.annotation.Required; 052 053 /** 054 * Service implementation for the MaintenanceDocument structure. This is the 055 * default implementation, that is delivered with Kuali 056 * 057 * @author Kuali Rice Team (rice.collab@kuali.org) 058 */ 059 @TransactionalNoValidationExceptionRollback 060 public class MaintenanceDocumentServiceImpl implements MaintenanceDocumentService { 061 private static final Logger LOG = Logger.getLogger(MaintenanceDocumentServiceImpl.class); 062 063 protected LegacyDataAdapter legacyDataAdapter; 064 protected DataObjectService dataObjectService; 065 protected DataObjectAuthorizationService dataObjectAuthorizationService; 066 protected DocumentService documentService; 067 protected DocumentDictionaryService documentDictionaryService; 068 069 /** 070 * @see org.kuali.rice.krad.service.MaintenanceDocumentService#setupNewMaintenanceDocument(java.lang.String, 071 * java.lang.String, java.lang.String) 072 */ 073 @Override 074 @SuppressWarnings("unchecked") 075 public MaintenanceDocument setupNewMaintenanceDocument(String objectClassName, String documentTypeName, 076 String maintenanceAction) { 077 if (StringUtils.isEmpty(objectClassName) && StringUtils.isEmpty(documentTypeName)) { 078 throw new IllegalArgumentException("Document type name or bo class not given!"); 079 } 080 081 // get document type if not passed 082 if (StringUtils.isEmpty(documentTypeName)) { 083 try { 084 documentTypeName = 085 getDocumentDictionaryService().getMaintenanceDocumentTypeName(Class.forName(objectClassName)); 086 } catch (ClassNotFoundException e) { 087 throw new RuntimeException(e); 088 } 089 090 if (StringUtils.isEmpty(documentTypeName)) { 091 throw new RuntimeException( 092 "documentTypeName is empty; does this Business Object have a maintenance document definition? " + 093 objectClassName); 094 } 095 } 096 097 // check doc type allows new or copy if that action was requested 098 if (KRADConstants.MAINTENANCE_NEW_ACTION.equals(maintenanceAction) || 099 KRADConstants.MAINTENANCE_COPY_ACTION.equals(maintenanceAction)) { 100 Class<?> boClass = 101 getDocumentDictionaryService().getMaintenanceDataObjectClass(documentTypeName); 102 boolean allowsNewOrCopy = getDataObjectAuthorizationService() 103 .canCreate(boClass, GlobalVariables.getUserSession().getPerson(), documentTypeName); 104 if (!allowsNewOrCopy) { 105 LOG.error("Document type " + documentTypeName + " does not allow new or copy actions."); 106 throw new DocumentTypeAuthorizationException( 107 GlobalVariables.getUserSession().getPerson().getPrincipalId(), "newOrCopy", documentTypeName); 108 } 109 } 110 111 // get new document from service 112 try { 113 return (MaintenanceDocument) getDocumentService().getNewDocument(documentTypeName); 114 } catch (WorkflowException e) { 115 LOG.error("Cannot get new maintenance document instance for doc type: " + documentTypeName, e); 116 throw new RuntimeException("Cannot get new maintenance document instance for doc type: " + documentTypeName, 117 e); 118 } 119 } 120 121 /** 122 * @see org.kuali.rice.krad.service.impl.MaintenanceDocumentServiceImpl#setupMaintenanceObject 123 */ 124 @Override 125 public void setupMaintenanceObject(MaintenanceDocument document, String maintenanceAction, 126 Map<String, String[]> requestParameters) { 127 document.getNewMaintainableObject().setMaintenanceAction(maintenanceAction); 128 document.getOldMaintainableObject().setMaintenanceAction(maintenanceAction); 129 130 // if action is delete, check that object can be deleted 131 if (KRADConstants.MAINTENANCE_DELETE_ACTION.equals(maintenanceAction)) 132 { 133 checkMaintenanceActionAuthorization(document, document.getOldMaintainableObject(), 134 maintenanceAction, requestParameters); 135 } 136 137 // if action is edit or copy first need to retrieve the old record 138 if (!KRADConstants.MAINTENANCE_NEW_ACTION.equals(maintenanceAction) && 139 !KRADConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equals(maintenanceAction)) { 140 Object oldDataObject = retrieveObjectForMaintenance(document, requestParameters); 141 142 Object newDataObject = null; 143 144 // TODO should we be using ObjectUtils? also, this needs dictionary 145 // enhancement to indicate fields to/not to copy 146 if (dataObjectService.supports(oldDataObject.getClass())) { 147 newDataObject = dataObjectService.copyInstance(oldDataObject); 148 } else { 149 newDataObject = SerializationUtils.deepCopy((Serializable) oldDataObject); 150 } 151 152 // set object instance for editing 153 document.getOldMaintainableObject().setDataObject(oldDataObject); 154 document.getNewMaintainableObject().setDataObject(newDataObject); 155 156 if (KRADConstants.MAINTENANCE_COPY_ACTION.equals(maintenanceAction) && !document.isFieldsClearedOnCopy()) { 157 Maintainable maintainable = document.getNewMaintainableObject(); 158 159 // Since this will be a new object, we also need to blank out the object ID and version number fields 160 // (if they exist). If the object uses a different locking key or unique ID field, the blanking of 161 // these will need to be done in the Maintainable.processAfterCopy() method. 162 if ( maintainable.getDataObject() instanceof DataObjectBase ) { 163 ((DataObjectBase) maintainable.getDataObject()).setObjectId(null); 164 ((DataObjectBase) maintainable.getDataObject()).setVersionNumber(null); 165 } else if ( maintainable.getDataObject() instanceof PersistableBusinessObject ) { 166 // Legacy KNS Support - since they don't use DataObjectBase 167 ((PersistableBusinessObject) maintainable.getDataObject()).setObjectId(null); 168 ((PersistableBusinessObject) maintainable.getDataObject()).setVersionNumber(null); 169 } else { 170 // If neither then use reflection to see if the object has setVersionNumber and setObjectId methods 171 if(ObjectPropertyUtils.getWriteMethod(maintainable.getDataObject().getClass(), "versionNumber") != null) { 172 ObjectPropertyUtils.setPropertyValue(maintainable.getDataObject(), "versionNumber", null); 173 } 174 175 if(ObjectPropertyUtils.getWriteMethod(maintainable.getDataObject().getClass(), "objectId") != null) { 176 ObjectPropertyUtils.setPropertyValue(maintainable.getDataObject(), "objectId", null); 177 } 178 } 179 180 if (!getDocumentDictionaryService().getPreserveLockingKeysOnCopy(maintainable.getDataObjectClass())) { 181 clearPrimaryKeyFields(newDataObject, maintainable.getDataObjectClass()); 182 } 183 } 184 185 checkMaintenanceActionAuthorization(document, oldDataObject, maintenanceAction, requestParameters); 186 } 187 188 // if new with existing we need to populate with passed in parameters 189 if (KRADConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equals(maintenanceAction)) { 190 Object newBO = document.getNewMaintainableObject().getDataObject(); 191 Map<String, String> parameters = 192 buildKeyMapFromRequest(requestParameters, document.getNewMaintainableObject().getDataObjectClass()); 193 ObjectPropertyUtils.copyPropertiesToObject(parameters, newBO); 194 if (newBO instanceof PersistableBusinessObject) { 195 ((PersistableBusinessObject) newBO).refresh(); 196 } 197 198 document.getNewMaintainableObject().setupNewFromExisting(document, requestParameters); 199 } else if (KRADConstants.MAINTENANCE_NEW_ACTION.equals(maintenanceAction)) { 200 document.getNewMaintainableObject().processAfterNew(document, requestParameters); 201 } 202 } 203 204 /** 205 * For the edit and delete maintenance actions checks with the 206 * <code>BusinessObjectAuthorizationService</code> to check whether the 207 * action is allowed for the record data. In action is allowed invokes the 208 * custom processing hook on the <code>Maintainble</code>. 209 * 210 * @param document - document instance for the maintenance object 211 * @param oldBusinessObject - the old maintenance record 212 * @param maintenanceAction - type of maintenance action requested 213 * @param requestParameters - map of parameters from the request 214 */ 215 protected void checkMaintenanceActionAuthorization(MaintenanceDocument document, Object oldBusinessObject, 216 String maintenanceAction, Map<String, String[]> requestParameters) { 217 if (KRADConstants.MAINTENANCE_EDIT_ACTION.equals(maintenanceAction)) { 218 boolean allowsEdit = getDataObjectAuthorizationService() 219 .canMaintain(oldBusinessObject, GlobalVariables.getUserSession().getPerson(), 220 document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName()); 221 if (!allowsEdit) { 222 LOG.error("Document type " + document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName() + 223 " does not allow edit actions."); 224 throw new DocumentTypeAuthorizationException( 225 GlobalVariables.getUserSession().getPerson().getPrincipalId(), "edit", 226 document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName()); 227 } 228 229 // invoke custom processing method 230 document.getNewMaintainableObject().processAfterEdit(document, requestParameters); 231 } else if (KRADConstants.MAINTENANCE_DELETE_ACTION.equals(maintenanceAction)) { 232 boolean allowsDelete = getDataObjectAuthorizationService() 233 .canMaintain(oldBusinessObject, GlobalVariables.getUserSession().getPerson(), 234 document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName()); 235 236 if (!allowsDelete) { 237 LOG.error("Document type " + document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName() + 238 " does not allow delete actions."); 239 throw new DocumentTypeAuthorizationException( 240 GlobalVariables.getUserSession().getPerson().getPrincipalId(), "delete", 241 document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName()); 242 } 243 244 boolean dataObjectAllowsDelete = getDocumentDictionaryService().getAllowsRecordDeletion( 245 document.getOldMaintainableObject().getDataObject().getClass()); 246 247 if (!dataObjectAllowsDelete) { 248 LOG.error("Document type " + document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName() + 249 " does not allow delete actions."); 250 GlobalVariables.getMessageMap().removeAllWarningMessagesForProperty(KRADConstants.GLOBAL_MESSAGES); 251 GlobalVariables.getMessageMap().putError(KRADConstants.DOCUMENT_ERRORS, 252 RiceKeyConstants.MESSAGE_DELETE_ACTION_NOT_SUPPORTED); 253 254 } 255 256 } 257 } 258 259 /** 260 * For the edit or copy actions retrieves the record that is to be 261 * maintained 262 * 263 * <p> 264 * Based on the persistence metadata for the maintenance object class 265 * retrieves the primary key values from the given request parameters map 266 * (if the class is persistable). With those key values attempts to find the 267 * record using the <code>LookupService</code>. 268 * </p> 269 * 270 * @param document - document instance for the maintenance object 271 * @param requestParameters - Map of parameters from the request 272 * @return Object the retrieved old object 273 */ 274 protected Object retrieveObjectForMaintenance(MaintenanceDocument document, 275 Map<String, String[]> requestParameters) { 276 Map<String, String> keyMap = 277 buildKeyMapFromRequest(requestParameters, document.getNewMaintainableObject().getDataObjectClass()); 278 279 Object oldDataObject = document.getNewMaintainableObject().retrieveObjectForEditOrCopy(document, keyMap); 280 281 if (oldDataObject == null && !document.getOldMaintainableObject().isExternalBusinessObject()) { 282 throw new RuntimeException( 283 "Cannot retrieve old record for maintenance document, incorrect parameters passed on maint url: " + 284 requestParameters); 285 } 286 287 if (document.getOldMaintainableObject().isExternalBusinessObject()) { 288 if (oldDataObject == null) { 289 try { 290 oldDataObject = document.getOldMaintainableObject().getDataObjectClass().newInstance(); 291 } catch (Exception ex) { 292 throw new RuntimeException( 293 "External BO maintainable was null and unable to instantiate for old maintainable object.", 294 ex); 295 } 296 } 297 298 populateMaintenanceObjectWithCopyKeyValues(KRADUtils.translateRequestParameterMap(requestParameters), 299 oldDataObject, document.getOldMaintainableObject()); 300 document.getOldMaintainableObject().prepareExternalBusinessObject((PersistableBusinessObject) oldDataObject); 301 oldDataObject = document.getOldMaintainableObject().getDataObject(); 302 } 303 304 return oldDataObject; 305 } 306 307 /** 308 * Clears the value of the primary key fields on the maintenance object 309 * 310 * @param maintenanceObject - document to clear the pk fields on 311 * @param dataObjectClass - class to use for retrieving primary key metadata 312 */ 313 protected void clearPrimaryKeyFields(Object maintenanceObject, Class<?> dataObjectClass) { 314 List<String> keyFieldNames = legacyDataAdapter.listPrimaryKeyFieldNames(dataObjectClass); 315 for (String keyFieldName : keyFieldNames) { 316 ObjectPropertyUtils.setPropertyValue(maintenanceObject, keyFieldName, null); 317 } 318 } 319 320 /** 321 * Based on the maintenance object class retrieves the key field names from 322 * the <code>BusinessObjectMetaDataService</code> (or alternatively from the 323 * request parameters), then retrieves any matching key value pairs from the 324 * request parameters 325 * 326 * @param requestParameters - map of parameters from the request 327 * @param dataObjectClass - class to use for checking security parameter restrictions 328 * @return Map<String, String> key value pairs 329 */ 330 protected Map<String, String> buildKeyMapFromRequest(Map<String, String[]> requestParameters, 331 Class<?> dataObjectClass) { 332 List<String> keyFieldNames = null; 333 334 // translate request parameters 335 Map<String, String> parameters = KRADUtils.translateRequestParameterMap(requestParameters); 336 337 // are override keys listed in the request? If so, then those need to be 338 // our keys, not the primary key fields for the BO 339 if (!StringUtils.isBlank(parameters.get(KRADConstants.OVERRIDE_KEYS))) { 340 String[] overrideKeys = 341 parameters.get(KRADConstants.OVERRIDE_KEYS).split(KRADConstants.FIELD_CONVERSIONS_SEPARATOR); 342 keyFieldNames = Arrays.asList(overrideKeys); 343 } else { 344 keyFieldNames = legacyDataAdapter.listPrimaryKeyFieldNames(dataObjectClass); 345 } 346 347 return KRADUtils.getParametersFromRequest(keyFieldNames, dataObjectClass, parameters); 348 } 349 350 /** 351 * Looks for a special request parameters giving the names of the keys that 352 * should be retrieved from the request parameters and copied to the 353 * maintenance object 354 * 355 * @param parameters - map of parameters from the request 356 * @param oldBusinessObject - the old maintenance object 357 * @param oldMaintainableObject - the old maintainble object (used to get object class for 358 * security checks) 359 */ 360 protected void populateMaintenanceObjectWithCopyKeyValues(Map<String, String> parameters, Object oldBusinessObject, 361 Maintainable oldMaintainableObject) { 362 List<String> keyFieldNamesToCopy = null; 363 Map<String, String> parametersToCopy = null; 364 365 if (!StringUtils.isBlank(parameters.get(KRADConstants.COPY_KEYS))) { 366 String[] copyKeys = 367 parameters.get(KRADConstants.COPY_KEYS).split(KRADConstants.FIELD_CONVERSIONS_SEPARATOR); 368 keyFieldNamesToCopy = Arrays.asList(copyKeys); 369 parametersToCopy = KRADUtils 370 .getParametersFromRequest(keyFieldNamesToCopy, oldMaintainableObject.getDataObjectClass(), 371 parameters); 372 } 373 374 if (parametersToCopy != null) { 375 // TODO: make sure we are doing formatting here eventually 376 ObjectPropertyUtils.copyPropertiesToObject(parametersToCopy, oldBusinessObject); 377 } 378 } 379 380 /** 381 * @see org.kuali.rice.krad.service.MaintenanceDocumentService#getLockingDocumentId(org.kuali.rice.krad.maintenance.MaintenanceDocument) 382 */ 383 @Override 384 public String getLockingDocumentId(MaintenanceDocument document) { 385 return getLockingDocumentId(document.getNewMaintainableObject(), document.getDocumentNumber()); 386 } 387 388 /** 389 * @see org.kuali.rice.krad.service.MaintenanceDocumentService#getLockingDocumentId(org.kuali.rice.krad.maintenance.Maintainable, 390 * java.lang.String) 391 */ 392 @Override 393 public String getLockingDocumentId(Maintainable maintainable, final String documentNumber) { 394 final List<MaintenanceLock> maintenanceLocks = maintainable.generateMaintenanceLocks(); 395 String lockingDocId = null; 396 for (MaintenanceLock maintenanceLock : maintenanceLocks) { 397 lockingDocId = getLockingDocumentNumber(maintenanceLock.getLockingRepresentation(), 398 documentNumber); 399 if (StringUtils.isNotBlank(lockingDocId)) { 400 break; 401 } 402 } 403 404 return lockingDocId; 405 } 406 407 protected String getLockingDocumentNumber(String lockingRepresentation, String documentNumber) { 408 String lockingDocNumber = ""; 409 410 // build the query criteria 411 List<Predicate> predicates = new ArrayList<Predicate>(); 412 predicates.add(PredicateFactory.equal("lockingRepresentation", lockingRepresentation)); 413 414 // if a docHeaderId is specified, then it will be excluded from the 415 // locking representation test. 416 if (StringUtils.isNotBlank(documentNumber)) { 417 predicates.add(PredicateFactory.notEqual(KRADPropertyConstants.DOCUMENT_NUMBER, documentNumber)); 418 } 419 420 QueryByCriteria.Builder qbc = QueryByCriteria.Builder.create(); 421 qbc.setPredicates(PredicateFactory.and(predicates.toArray(new Predicate[predicates.size()]))); 422 423 // attempt to retrieve a document based off this criteria 424 List<MaintenanceLock> results = KradDataServiceLocator.getDataObjectService().findMatching(MaintenanceLock.class, qbc.build()) 425 .getResults(); 426 if (results.size() > 1) { 427 throw new IllegalStateException( 428 "Expected single result querying for MaintenanceLock. LockRep: " + lockingRepresentation); 429 } 430 431 // if a document was found, then there's already one out there pending, 432 // and we consider it 'locked' and we return the docnumber. 433 if (!results.isEmpty()) { 434 lockingDocNumber = results.get(0).getDocumentNumber(); 435 } 436 return lockingDocNumber; 437 } 438 439 /** 440 * @see org.kuali.rice.krad.service.MaintenanceDocumentService#deleteLocks(String) 441 */ 442 @Override 443 public void deleteLocks(String documentNumber) { 444 dataObjectService.deleteMatching(MaintenanceLock.class, QueryByCriteria.Builder.forAttribute( 445 "documentNumber", documentNumber).build()); 446 } 447 448 /** 449 * @see org.kuali.rice.krad.service.MaintenanceDocumentService#storeLocks(java.util.List) 450 */ 451 @Override 452 public void storeLocks(List<MaintenanceLock> maintenanceLocks) { 453 if (maintenanceLocks == null) { 454 return; 455 } 456 for (MaintenanceLock maintenanceLock : maintenanceLocks) { 457 dataObjectService.save(maintenanceLock); 458 } 459 } 460 461 protected DataObjectAuthorizationService getDataObjectAuthorizationService() { 462 return dataObjectAuthorizationService; 463 } 464 465 @Required 466 public void setDataObjectAuthorizationService(DataObjectAuthorizationService dataObjectAuthorizationService) { 467 this.dataObjectAuthorizationService = dataObjectAuthorizationService; 468 } 469 470 protected DocumentService getDocumentService() { 471 return this.documentService; 472 } 473 474 @Required 475 public void setDocumentService(DocumentService documentService) { 476 this.documentService = documentService; 477 } 478 479 public DocumentDictionaryService getDocumentDictionaryService() { 480 return documentDictionaryService; 481 } 482 483 @Required 484 public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) { 485 this.documentDictionaryService = documentDictionaryService; 486 } 487 488 @Required 489 public void setDataObjectService(DataObjectService dataObjectService) { 490 this.dataObjectService = dataObjectService; 491 } 492 493 @Required 494 public void setLegacyDataAdapter(LegacyDataAdapter legacyDataAdapter) { 495 this.legacyDataAdapter = legacyDataAdapter; 496 } 497 498 }