001/** 002 * Copyright 2005-2016 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.service.impl; 017 018import org.apache.commons.lang.StringUtils; 019import org.apache.ojb.broker.OptimisticLockException; 020import org.kuali.rice.core.api.util.RiceConstants; 021import org.kuali.rice.kim.api.KimConstants.PermissionNames; 022import org.kuali.rice.kim.api.identity.Person; 023import org.kuali.rice.kim.api.identity.PersonService; 024import org.kuali.rice.kim.api.permission.PermissionService; 025import org.kuali.rice.kim.api.services.KimApiServiceLocator; 026import org.kuali.rice.kns.authorization.AuthorizationConstants; 027import org.kuali.rice.krad.UserSession; 028import org.kuali.rice.krad.document.Document; 029import org.kuali.rice.krad.document.authorization.PessimisticLock; 030import org.kuali.rice.krad.exception.AuthorizationException; 031import org.kuali.rice.krad.exception.PessimisticLockingException; 032import org.kuali.rice.krad.service.BusinessObjectService; 033import org.kuali.rice.krad.service.DataDictionaryService; 034import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 035import org.kuali.rice.krad.service.PessimisticLockService; 036import org.kuali.rice.krad.util.GlobalVariables; 037import org.kuali.rice.krad.util.KRADConstants; 038import org.kuali.rice.krad.util.KRADPropertyConstants; 039import org.kuali.rice.krad.util.ObjectUtils; 040import org.springframework.transaction.annotation.Transactional; 041 042import java.util.ArrayList; 043import java.util.Collections; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.Iterator; 047import java.util.List; 048import java.util.Map; 049import java.util.Set; 050 051/** 052 * This is a service implementation for pessimistic locking 053 * 054 * @author Kuali Rice Team (rice.collab@kuali.org) 055 * 056 */ 057@Transactional 058public class PessimisticLockServiceImpl implements PessimisticLockService { 059 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PessimisticLockServiceImpl.class); 060 061 private PersonService personService; 062 private BusinessObjectService businessObjectService; 063 private DataDictionaryService dataDictionaryService; 064 private PermissionService permissionService; 065 066 /** 067 * @see org.kuali.rice.krad.service.PessimisticLockService#delete(java.lang.String) 068 */ 069 public void delete(String id) { 070 if (StringUtils.isBlank(id)) { 071 throw new IllegalArgumentException("An invalid blank id was passed to delete a Pessimistic Lock."); 072 } 073 Map<String,Object> primaryKeys = new HashMap<String,Object>(); 074 primaryKeys.put(KRADPropertyConstants.ID, Long.valueOf(id)); 075 PessimisticLock lock = (PessimisticLock) getBusinessObjectService().findByPrimaryKey(PessimisticLock.class, primaryKeys); 076 if (ObjectUtils.isNull(lock)) { 077 throw new IllegalArgumentException("Pessimistic Lock with id " + id + " cannot be found in the database."); 078 } 079 Person user = GlobalVariables.getUserSession().getPerson(); 080 if ( (!lock.isOwnedByUser(user)) && (!isPessimisticLockAdminUser(user)) ) { 081 throw new AuthorizationException(user.getName(),"delete", "Pessimistick Lock (id " + id + ")"); 082 } 083 delete(lock); 084 } 085 086 private void delete(PessimisticLock lock) { 087 if ( LOG.isDebugEnabled() ) { 088 LOG.debug("Deleting lock: " + lock); 089 } 090 getBusinessObjectService().delete(lock); 091 } 092 093 /** 094 * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(String) 095 */ 096 public PessimisticLock generateNewLock(String documentNumber) { 097 return generateNewLock(documentNumber, GlobalVariables.getUserSession().getPerson()); 098 } 099 100 /** 101 * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(java.lang.String) 102 */ 103 public PessimisticLock generateNewLock(String documentNumber, String lockDescriptor) { 104 return generateNewLock(documentNumber, lockDescriptor, GlobalVariables.getUserSession().getPerson()); 105 } 106 107 /** 108 * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(java.lang.String, org.kuali.rice.kim.api.identity.Person) 109 */ 110 public PessimisticLock generateNewLock(String documentNumber, Person user) { 111 return generateNewLock(documentNumber, PessimisticLock.DEFAULT_LOCK_DESCRIPTOR, user); 112 } 113 114 /** 115 * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(java.lang.String, java.lang.String, org.kuali.rice.kim.api.identity.Person) 116 */ 117 public PessimisticLock generateNewLock(String documentNumber, String lockDescriptor, Person user) { 118 PessimisticLock lock = new PessimisticLock(documentNumber, lockDescriptor, user, GlobalVariables.getUserSession()); 119 lock = save(lock); 120 if ( LOG.isDebugEnabled() ) { 121 LOG.debug("Generated new lock: " + lock); 122 } 123 return lock; 124 } 125 126 /** 127 * @see org.kuali.rice.krad.service.PessimisticLockService#getPessimisticLocksForDocument(java.lang.String) 128 */ 129 public List<PessimisticLock> getPessimisticLocksForDocument(String documentNumber) { 130 Map fieldValues = new HashMap(); 131 fieldValues.put(KRADPropertyConstants.DOCUMENT_NUMBER, documentNumber); 132 return (List<PessimisticLock>) getBusinessObjectService().findMatching(PessimisticLock.class, fieldValues); 133 } 134 135 /** 136 * @see org.kuali.rice.krad.service.PessimisticLockService#getPessimisticLocksForSession(java.lang.String) 137 */ 138 public List<PessimisticLock> getPessimisticLocksForSession(String sessionId) { 139 Map fieldValues = new HashMap(); 140 fieldValues.put(KRADPropertyConstants.SESSION_ID, sessionId); 141 return (List<PessimisticLock>) getBusinessObjectService().findMatching(PessimisticLock.class, fieldValues); 142 } 143 144 /** 145 * @see org.kuali.rice.krad.service.PessimisticLockService#isPessimisticLockAdminUser(org.kuali.rice.kim.api.identity.Person) 146 */ 147 public boolean isPessimisticLockAdminUser(Person user) { 148 return getPermissionService().isAuthorized( user.getPrincipalId(), KRADConstants.KNS_NAMESPACE, PermissionNames.ADMIN_PESSIMISTIC_LOCKING, 149 Collections.<String, String>emptyMap() ); 150 } 151 152 /** 153 * @see org.kuali.rice.krad.service.PessimisticLockService#releaseAllLocksForUser(java.util.List, org.kuali.rice.kim.api.identity.Person) 154 */ 155 public void releaseAllLocksForUser(List<PessimisticLock> locks, Person user) { 156 for (Iterator<PessimisticLock> iterator = locks.iterator(); iterator.hasNext();) { 157 PessimisticLock lock = (PessimisticLock) iterator.next(); 158 if (lock.isOwnedByUser(user)) { 159 try { 160 delete(lock); 161 } catch ( RuntimeException ex ) { 162 if ( ex.getCause() instanceof OptimisticLockException) { 163 LOG.warn( "Suppressing Optimistic Lock Exception. Document Num: " + lock.getDocumentNumber()); 164 } else { 165 throw ex; 166 } 167 } 168 } 169 } 170 } 171 172 /** 173 * @see org.kuali.rice.krad.service.PessimisticLockService#releaseAllLocksForUser(java.util.List, org.kuali.rice.kim.api.identity.Person, java.lang.String) 174 */ 175 public void releaseAllLocksForUser(List<PessimisticLock> locks, Person user, String lockDescriptor) { 176 for (Iterator<PessimisticLock> iterator = locks.iterator(); iterator.hasNext();) { 177 PessimisticLock lock = (PessimisticLock) iterator.next(); 178 if ( (lock.isOwnedByUser(user)) && (lockDescriptor.equals(lock.getLockDescriptor())) ) { 179 try { 180 delete(lock); 181 } catch ( RuntimeException ex ) { 182 if ( ex.getCause() instanceof OptimisticLockException ) { 183 LOG.warn( "Suppressing Optimistic Lock Exception. Document Num: " + lock.getDocumentNumber()); 184 } else { 185 throw ex; 186 } 187 } 188 } 189 } 190 } 191 192 /** 193 * @see org.kuali.rice.krad.service.PessimisticLockService#save(org.kuali.rice.krad.document.authorization.PessimisticLock) 194 */ 195 public PessimisticLock save(PessimisticLock lock) { 196 if ( LOG.isDebugEnabled() ) { 197 LOG.debug("Saving lock: " + lock); 198 } 199 return (PessimisticLock)getBusinessObjectService().save(lock); 200 } 201 202 public BusinessObjectService getBusinessObjectService() { 203 return this.businessObjectService; 204 } 205 206 public void setBusinessObjectService(BusinessObjectService businessObjectService) { 207 this.businessObjectService = businessObjectService; 208 } 209 210 /** 211 * @param document 212 * @param user 213 * @return Set of actions are permitted the given user on the given document 214 */ 215 public Set getDocumentActions(Document document, Person user, Set<String> documentActions){ 216 if(documentActions.contains(KRADConstants.KUALI_ACTION_CAN_CANCEL) && !hasPreRouteEditAuthorization(document, user) ){ 217 documentActions.remove(KRADConstants.KUALI_ACTION_CAN_CANCEL); 218 } 219 if(documentActions.contains(KRADConstants.KUALI_ACTION_CAN_SAVE) && !hasPreRouteEditAuthorization(document, user)){ 220 documentActions.remove(KRADConstants.KUALI_ACTION_CAN_SAVE); 221 } 222 if(documentActions.contains(KRADConstants.KUALI_ACTION_CAN_ROUTE) && !hasPreRouteEditAuthorization(document, user)){ 223 documentActions.remove(KRADConstants.KUALI_ACTION_CAN_ROUTE); 224 } 225 if (documentActions.contains(KRADConstants.KUALI_ACTION_CAN_BLANKET_APPROVE) && !hasPreRouteEditAuthorization(document, user)){ 226 documentActions.remove(KRADConstants.KUALI_ACTION_CAN_BLANKET_APPROVE); 227 } 228 return documentActions; 229 } 230 231 232 /** 233 * This method checks to see that the given user has a lock on the document and return true if one is found. 234 * 235 * @param document - document to check 236 * @param user - current user 237 * @return true if the document is using Pessimistic Locking, the user has initiate authorization (see 238 * {@link #hasInitiateAuthorization(Document, Person)}), and the document has a lock owned by the given 239 * user. If the document is not using Pessimistic Locking the value returned will be that returned by 240 * {@link #hasInitiateAuthorization(Document, Person)}. 241 */ 242 protected boolean hasPreRouteEditAuthorization(Document document, Person user) { 243 if (document.getPessimisticLocks().isEmpty()) { 244 return true; 245 } 246 for (Iterator iterator = document.getPessimisticLocks().iterator(); iterator.hasNext();) { 247 PessimisticLock lock = (PessimisticLock) iterator.next(); 248 if (lock.isOwnedByUser(user)) { 249 return true; 250 } 251 } 252 return false; 253 } 254 255 256 protected boolean usesPessimisticLocking(Document document) { 257 return getDataDictionaryService().getDataDictionary().getDocumentEntry(document.getClass().getName()).getUsePessimisticLocking(); 258 } 259 260 261 /** 262 * This method creates a new {@link PessimisticLock} when Workflow processing requires one 263 * 264 * @param document - the document to create the lock against and add the lock to 265 * @see org.kuali.rice.kns.document.authorization.DocumentAuthorizer#establishWorkflowPessimisticLocking(org.kuali.rice.krad.document.Document) 266 */ 267 public void establishWorkflowPessimisticLocking(Document document) { 268 PessimisticLock lock = createNewPessimisticLock(document, new HashMap(), getWorkflowPessimisticLockOwnerUser()); 269 document.addPessimisticLock(lock); 270 } 271 272 /** 273 * This method releases locks created via the {@link #establishWorkflowPessimisticLocking(Document)} method for the given document 274 * 275 * @param document - document to release locks from 276 * @see org.kuali.rice.kns.document.authorization.DocumentAuthorizer#releaseWorkflowPessimisticLocking(org.kuali.rice.krad.document.Document) 277 */ 278 public void releaseWorkflowPessimisticLocking(Document document) { 279 releaseAllLocksForUser(document.getPessimisticLocks(), getWorkflowPessimisticLockOwnerUser()); 280 document.refreshPessimisticLocks(); 281 } 282 283 /** 284 * This method identifies the user that should be used to create and clear {@link PessimisticLock} objects required by 285 * Workflow.<br> 286 * <br> 287 * The default is the Kuali system user defined by {@link RiceConstants#SYSTEM_USER}. This method can be overriden by 288 * implementing documents if another user is needed. 289 * 290 * @return a valid {@link Person} object 291 */ 292 protected Person getWorkflowPessimisticLockOwnerUser() { 293 String networkId = KRADConstants.SYSTEM_USER; 294 return getPersonService().getPersonByPrincipalName(networkId); 295 } 296 297 /** 298 * This implementation will check the given document, editMode map, and user object to verify Pessimistic Locking. If the 299 * given edit mode map contains an 'entry type' edit mode then the system will check the locks already in existence on 300 * the document. If a valid lock for the given user is found the system will return the given edit mode map. If a valid 301 * lock is found but is owned by another user the edit mode map returned will have any 'entry type' edit modes removed. If the 302 * given document has no locks and the edit mode map passed in has at least one 'entry type' mode then a new 303 * {@link PessimisticLock} object will be created and set on the document for the given user.<br> 304 * <br> 305 * NOTE: This method is only called if the document uses pessimistic locking as described in the data dictionary file. 306 * 307 * @see org.kuali.rice.kns.document.authorization.DocumentAuthorizer#establishLocks(org.kuali.rice.krad.document.Document, 308 * java.util.Map, org.kuali.rice.kim.api.identity.Person) 309 */ 310 public Map establishLocks(Document document, Map editMode, Person user) { 311 Map editModeMap = new HashMap(); 312 // givenUserLockDescriptors is a list of lock descriptors currently held on the document by the given user 313 List<String> givenUserLockDescriptors = new ArrayList<String>(); 314 // lockDescriptorUsers is a map with lock descriptors as keys and users other than the given user who hold a lock of each descriptor 315 Map<String,Set<Person>> lockDescriptorUsers = new HashMap<String,Set<Person>>(); 316 317 // build the givenUserLockDescriptors set and the lockDescriptorUsers map 318 for (PessimisticLock lock : document.getPessimisticLocks()) { 319 if (lock.isOwnedByUser(user)) { 320 // lock is owned by given user 321 givenUserLockDescriptors.add(lock.getLockDescriptor()); 322 } else { 323 // lock is not owned by the given user 324 if (!lockDescriptorUsers.containsKey(lock.getLockDescriptor())) { 325 lockDescriptorUsers.put(lock.getLockDescriptor(), new HashSet<Person>()); 326 } 327 ((Set<Person>) lockDescriptorUsers.get(lock.getLockDescriptor())).add(lock.getOwnedByUser()); 328 } 329 } 330 331 // verify that no locks held by current user exist for any other user 332 for (String givenUserLockDescriptor : givenUserLockDescriptors) { 333 if ( (lockDescriptorUsers.containsKey(givenUserLockDescriptor)) && (lockDescriptorUsers.get(givenUserLockDescriptor).size() > 0) ) { 334 Set<Person> users = lockDescriptorUsers.get(givenUserLockDescriptor); 335 if ( (users.size() != 1) || (!getWorkflowPessimisticLockOwnerUser().getPrincipalId().equals(users.iterator().next().getPrincipalId())) ) { 336 String descriptorText = (document.useCustomLockDescriptors()) ? " using lock descriptor '" + givenUserLockDescriptor + "'" : ""; 337 String errorMsg = "Found an invalid lock status on document number " + document.getDocumentNumber() + "with current user and other user both having locks" + descriptorText + " concurrently"; 338 LOG.debug(errorMsg); 339 throw new PessimisticLockingException(errorMsg); 340 } 341 } 342 } 343 344 // check to see if the given user has any locks in the system at all 345 if (givenUserLockDescriptors.isEmpty()) { 346 // given user has no locks... check for other user locks 347 if (lockDescriptorUsers.isEmpty()) { 348 // no other user has any locks... set up locks for given user if user has edit privileges 349 if (isLockRequiredByUser(document, editMode, user)) { 350 document.addPessimisticLock(createNewPessimisticLock(document, editMode, user)); 351 } 352 editModeMap.putAll(editMode); 353 } else { 354 // at least one other user has at least one other lock... adjust edit mode for read only 355 if (document.useCustomLockDescriptors()) { 356 // check to see if the custom lock descriptor is already in use 357 String customLockDescriptor = document.getCustomLockDescriptor(user); 358 if (lockDescriptorUsers.containsKey(customLockDescriptor)) { 359 // at least one other user has this descriptor locked... remove editable edit modes 360 editModeMap = getEditModeWithEditableModesRemoved(editMode); 361 } else { 362 // no other user has a lock with this descriptor 363 if (isLockRequiredByUser(document, editMode, user)) { 364 document.addPessimisticLock(createNewPessimisticLock(document, editMode, user)); 365 } 366 editModeMap.putAll(editMode); 367 } 368 } else { 369 editModeMap = getEditModeWithEditableModesRemoved(editMode); 370 } 371 } 372 } else { 373 // given user already has at least one lock descriptor 374 if (document.useCustomLockDescriptors()) { 375 // get the custom lock descriptor and check to see if if the given user has a lock with that descriptor 376 String customLockDescriptor = document.getCustomLockDescriptor(user); 377 if (givenUserLockDescriptors.contains(customLockDescriptor)) { 378 // user already has lock that is required 379 editModeMap.putAll(editMode); 380 } else { 381 // user does not have lock for descriptor required 382 if (lockDescriptorUsers.containsKey(customLockDescriptor)) { 383 // another user has the lock descriptor that the given user requires... disallow lock and alter edit modes to have read only 384 editModeMap = getEditModeWithEditableModesRemoved(editMode); 385 } else { 386 // no other user has a lock with this descriptor... check if this user needs a lock 387 if (isLockRequiredByUser(document, editMode, user)) { 388 document.addPessimisticLock(createNewPessimisticLock(document, editMode, user)); 389 } 390 editModeMap.putAll(editMode); 391 } 392 } 393 } else { 394 // user already has lock and no descriptors are being used... use the existing edit modes 395 editModeMap.putAll(editMode); 396 } 397 } 398 399 return editModeMap; 400 } 401 402 /** 403 * This method is used to check if the given parameters warrant a new lock to be created for the given user. This method 404 * utilizes the {@link #isEntryEditMode(java.util.Map.Entry)} method. 405 * 406 * @param document - 407 * document to verify lock creation against 408 * @param editMode - 409 * edit modes list to check for 'entry type' edit modes 410 * @param user - 411 * user the lock will be 'owned' by 412 * @return true if the given edit mode map has at least one 'entry type' edit mode... false otherwise 413 */ 414 protected boolean isLockRequiredByUser(Document document, Map editMode, Person user) { 415 // check for entry edit mode 416 for (Iterator iterator = editMode.entrySet().iterator(); iterator.hasNext();) { 417 Map.Entry entry = (Map.Entry) iterator.next(); 418 if (isEntryEditMode(entry)) { 419 return true; 420 } 421 } 422 return false; 423 } 424 425 /** 426 * This method is used to remove edit modes from the given map that allow the user to edit data on the document. This 427 * method utilizes the {@link #isEntryEditMode(java.util.Map.Entry)} method to identify if an edit mode is defined as an 428 * 'entry type' edit mode. It also uses the {@link #getEntryEditModeReplacementMode(java.util.Map.Entry)} method to replace 429 * any 'entry type' edit modes it finds. 430 * 431 * @param currentEditMode - 432 * current set of edit modes the user has assigned to them 433 * @return an adjusted edit mode map where 'entry type' edit modes have been removed or replaced using the 434 * {@link #getEntryEditModeReplacementMode} method 435 */ 436 protected Map getEditModeWithEditableModesRemoved(Map currentEditMode) { 437 Map editModeMap = new HashMap(); 438 for (Iterator iterator = currentEditMode.entrySet().iterator(); iterator.hasNext();) { 439 Map.Entry entry = (Map.Entry) iterator.next(); 440 if (isEntryEditMode(entry)) { 441 editModeMap.putAll(getEntryEditModeReplacementMode(entry)); 442 } else { 443 editModeMap.put(entry.getKey(), entry.getValue()); 444 } 445 } 446 return editModeMap; 447 } 448 449 /** 450 * This method is used to check if the given {@link Map.Entry} is an 'entry type' edit mode and that the value is set to 451 * signify that this user has that edit mode available to them 452 * 453 * @param entry - 454 * the {@link Map.Entry} object that contains an edit mode such as the ones returned but 455 * {@link #getEditMode(Document, Person)} 456 * @return true if the given entry has a key signifying an 'entry type' edit mode and the value is equal to 457 * {@link #EDIT_MODE_DEFAULT_TRUE_VALUE}... false if not 458 */ 459 protected boolean isEntryEditMode(Map.Entry entry) { 460 // check for FULL_ENTRY edit mode set to default true value 461 if (AuthorizationConstants.EditMode.FULL_ENTRY.equals(entry.getKey())) { 462 String fullEntryEditModeValue = (String)entry.getValue(); 463 return ( StringUtils.equalsIgnoreCase(KRADConstants.KUALI_DEFAULT_TRUE_VALUE, fullEntryEditModeValue) ); 464 } 465 return false; 466 } 467 468 /** 469 * This method is used to return values needed to replace the given 'entry type' edit mode {@link Map.Entry} with one that will not allow the user to enter data on the document 470 * 471 * @param entry - the current 'entry type' edit mode to replace 472 * @return a Map of edit modes that will be used to replace this edit mode (represented by the given entry parameter) 473 */ 474 protected Map getEntryEditModeReplacementMode(Map.Entry entry) { 475 Map editMode = new HashMap(); 476 editMode.put(AuthorizationConstants.EditMode.VIEW_ONLY, KRADConstants.KUALI_DEFAULT_TRUE_VALUE); 477 return editMode; 478 } 479 480 /** 481 * This method creates a new {@link PessimisticLock} object using the given document and user. If the document's 482 * useCustomLockDescriptors() method returns true then the new lock will also have a custom lock descriptor 483 * value set to the return value of the document's getCustomLockDescriptor(Person) method. 484 * 485 * @param document - 486 * document to place the lock on 487 * @param editMode - 488 * current edit modes for given user 489 * @param user - 490 * user who will 'own' the new lock object 491 * @return the newly created lock object 492 */ 493 protected PessimisticLock createNewPessimisticLock(Document document, Map editMode, Person user) { 494 if (document.useCustomLockDescriptors()) { 495 return generateNewLock(document.getDocumentNumber(), document.getCustomLockDescriptor(user), user); 496 } else { 497 return generateNewLock(document.getDocumentNumber(), user); 498 } 499 } 500 501 public PersonService getPersonService() { 502 if ( personService == null ) { 503 personService = KimApiServiceLocator.getPersonService(); 504 } 505 return personService; 506 } 507 508 public DataDictionaryService getDataDictionaryService() { 509 if ( dataDictionaryService == null ) { 510 dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService(); 511 } 512 return dataDictionaryService; 513 } 514 515 public PermissionService getPermissionService() { 516 if ( permissionService == null ) { 517 permissionService = KimApiServiceLocator.getPermissionService(); 518 } 519 return permissionService; 520 } 521 522 523 524} 525