View Javadoc

1   /**
2    * Copyright 2005-2014 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.service.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.ojb.broker.OptimisticLockException;
20  import org.kuali.rice.core.api.util.RiceConstants;
21  import org.kuali.rice.kim.api.KimConstants.PermissionNames;
22  import org.kuali.rice.kim.api.identity.Person;
23  import org.kuali.rice.kim.api.identity.PersonService;
24  import org.kuali.rice.kim.api.permission.PermissionService;
25  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
26  import org.kuali.rice.kns.authorization.AuthorizationConstants;
27  import org.kuali.rice.krad.UserSession;
28  import org.kuali.rice.krad.document.Document;
29  import org.kuali.rice.krad.document.authorization.PessimisticLock;
30  import org.kuali.rice.krad.exception.AuthorizationException;
31  import org.kuali.rice.krad.exception.PessimisticLockingException;
32  import org.kuali.rice.krad.service.BusinessObjectService;
33  import org.kuali.rice.krad.service.DataDictionaryService;
34  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
35  import org.kuali.rice.krad.service.PessimisticLockService;
36  import org.kuali.rice.krad.util.GlobalVariables;
37  import org.kuali.rice.krad.util.KRADConstants;
38  import org.kuali.rice.krad.util.KRADPropertyConstants;
39  import org.kuali.rice.krad.util.ObjectUtils;
40  import org.springframework.transaction.annotation.Transactional;
41  
42  import java.util.ArrayList;
43  import java.util.Collections;
44  import java.util.HashMap;
45  import java.util.HashSet;
46  import java.util.Iterator;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Set;
50  
51  /**
52   * This is a service implementation for pessimistic locking
53   *
54   * @author Kuali Rice Team (rice.collab@kuali.org)
55   *
56   */
57  @Transactional
58  public class PessimisticLockServiceImpl implements PessimisticLockService {
59      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PessimisticLockServiceImpl.class);
60  
61      private PersonService personService;
62      private BusinessObjectService businessObjectService;
63      private DataDictionaryService dataDictionaryService;
64      private PermissionService permissionService;
65  
66      /**
67       * @see org.kuali.rice.krad.service.PessimisticLockService#delete(java.lang.String)
68       */
69      public void delete(String id) {
70          if (StringUtils.isBlank(id)) {
71              throw new IllegalArgumentException("An invalid blank id was passed to delete a Pessimistic Lock.");
72          }
73          Map<String,Object> primaryKeys = new HashMap<String,Object>();
74          primaryKeys.put(KRADPropertyConstants.ID, Long.valueOf(id));
75          PessimisticLock lock = (PessimisticLock) getBusinessObjectService().findByPrimaryKey(PessimisticLock.class, primaryKeys);
76          if (ObjectUtils.isNull(lock)) {
77              throw new IllegalArgumentException("Pessimistic Lock with id " + id + " cannot be found in the database.");
78          }
79          Person user = GlobalVariables.getUserSession().getPerson();
80          if ( (!lock.isOwnedByUser(user)) && (!isPessimisticLockAdminUser(user)) ) {
81              throw new AuthorizationException(user.getName(),"delete", "Pessimistick Lock (id " + id + ")");
82          }
83          delete(lock);
84      }
85  
86      private void delete(PessimisticLock lock) {
87      	if ( LOG.isDebugEnabled() ) {
88      		LOG.debug("Deleting lock: " + lock);
89      	}
90          getBusinessObjectService().delete(lock);
91      }
92  
93      /**
94       * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(String)
95       */
96      public PessimisticLock generateNewLock(String documentNumber) {
97          return generateNewLock(documentNumber, GlobalVariables.getUserSession().getPerson());
98      }
99  
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