View Javadoc

1   /**
2    * Copyright 2005-2011 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.kuali.rice.core.api.util.RiceConstants;
20  import org.kuali.rice.kim.api.KimConstants.PermissionNames;
21  import org.kuali.rice.kim.api.identity.Person;
22  import org.kuali.rice.kim.api.identity.PersonService;
23  import org.kuali.rice.kim.api.permission.PermissionService;
24  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
25  import org.kuali.rice.krad.authorization.AuthorizationConstants;
26  import org.kuali.rice.krad.document.Document;
27  import org.kuali.rice.krad.document.authorization.PessimisticLock;
28  import org.kuali.rice.krad.exception.AuthorizationException;
29  import org.kuali.rice.krad.exception.PessimisticLockingException;
30  import org.kuali.rice.krad.service.BusinessObjectService;
31  import org.kuali.rice.krad.service.DataDictionaryService;
32  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
33  import org.kuali.rice.krad.service.PessimisticLockService;
34  import org.kuali.rice.krad.util.GlobalVariables;
35  import org.kuali.rice.krad.util.KRADConstants;
36  import org.kuali.rice.krad.util.KRADPropertyConstants;
37  import org.kuali.rice.krad.util.ObjectUtils;
38  import org.springframework.transaction.annotation.Transactional;
39  
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.HashMap;
43  import java.util.HashSet;
44  import java.util.Iterator;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Set;
48  
49  /**
50   * This is a service implementation for pessimistic locking
51   *
52   * @author Kuali Rice Team (rice.collab@kuali.org)
53   *
54   */
55  @Transactional
56  public class PessimisticLockServiceImpl implements PessimisticLockService {
57      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PessimisticLockServiceImpl.class);
58  
59      private PersonService personService;
60      private BusinessObjectService businessObjectService;
61      private DataDictionaryService dataDictionaryService;
62      private PermissionService permissionService;
63  
64      /**
65       * @see org.kuali.rice.krad.service.PessimisticLockService#delete(java.lang.String)
66       */
67      public void delete(String id) {
68          if (StringUtils.isBlank(id)) {
69              throw new IllegalArgumentException("An invalid blank id was passed to delete a Pessimistic Lock.");
70          }
71          Map<String,Object> primaryKeys = new HashMap<String,Object>();
72          primaryKeys.put(KRADPropertyConstants.ID, Long.valueOf(id));
73          PessimisticLock lock = (PessimisticLock) getBusinessObjectService().findByPrimaryKey(PessimisticLock.class, primaryKeys);
74          if (ObjectUtils.isNull(lock)) {
75              throw new IllegalArgumentException("Pessimistic Lock with id " + id + " cannot be found in the database.");
76          }
77          Person user = GlobalVariables.getUserSession().getPerson();
78          if ( (!lock.isOwnedByUser(user)) && (!isPessimisticLockAdminUser(user)) ) {
79              throw new AuthorizationException(user.getName(),"delete", "Pessimistick Lock (id " + id + ")");
80          }
81          delete(lock);
82      }
83  
84      private void delete(PessimisticLock lock) {
85      	if ( LOG.isDebugEnabled() ) {
86      		LOG.debug("Deleting lock: " + lock);
87      	}
88          getBusinessObjectService().delete(lock);
89      }
90  
91      /**
92       * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(String)
93       */
94      public PessimisticLock generateNewLock(String documentNumber) {
95          return generateNewLock(documentNumber, GlobalVariables.getUserSession().getPerson());
96      }
97  
98      /**
99       * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(java.lang.String)
100      */
101     public PessimisticLock generateNewLock(String documentNumber, String lockDescriptor) {
102         return generateNewLock(documentNumber, lockDescriptor, GlobalVariables.getUserSession().getPerson());
103     }
104 
105     /**
106      * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(java.lang.String, org.kuali.rice.kim.api.identity.Person)
107      */
108     public PessimisticLock generateNewLock(String documentNumber, Person user) {
109         return generateNewLock(documentNumber, PessimisticLock.DEFAULT_LOCK_DESCRIPTOR, user);
110     }
111 
112     /**
113      * @see org.kuali.rice.krad.service.PessimisticLockService#generateNewLock(java.lang.String, java.lang.String, org.kuali.rice.kim.api.identity.Person)
114      */
115     public PessimisticLock generateNewLock(String documentNumber, String lockDescriptor, Person user) {
116         PessimisticLock lock = new PessimisticLock(documentNumber, lockDescriptor, user);
117         lock = save(lock);
118         if ( LOG.isDebugEnabled() ) {
119         	LOG.debug("Generated new lock: " + lock);
120         }
121         return lock;
122     }
123 
124     /**
125      * @see org.kuali.rice.krad.service.PessimisticLockService#getPessimisticLocksForDocument(java.lang.String)
126      */
127     @SuppressWarnings("unchecked")
128     public List<PessimisticLock> getPessimisticLocksForDocument(String documentNumber) {
129         Map fieldValues = new HashMap();
130         fieldValues.put(KRADPropertyConstants.DOCUMENT_NUMBER, documentNumber);
131         return (List<PessimisticLock>) getBusinessObjectService().findMatching(PessimisticLock.class, fieldValues);
132     }
133 
134     /**
135      * @see org.kuali.rice.krad.service.PessimisticLockService#isPessimisticLockAdminUser(org.kuali.rice.kim.api.identity.Person)
136      */
137     public boolean isPessimisticLockAdminUser(Person user) {
138     	return getPermissionService().isAuthorized( user.getPrincipalId(), KRADConstants.KRAD_NAMESPACE, PermissionNames.ADMIN_PESSIMISTIC_LOCKING,
139                 Collections.<String, String>emptyMap(), Collections.<String, String>emptyMap() );
140     }
141 
142     /**
143      * @see org.kuali.rice.krad.service.PessimisticLockService#releaseAllLocksForUser(java.util.List, org.kuali.rice.kim.api.identity.Person)
144      */
145     public void releaseAllLocksForUser(List<PessimisticLock> locks, Person user) {
146         for (Iterator<PessimisticLock> iterator = locks.iterator(); iterator.hasNext();) {
147             PessimisticLock lock = (PessimisticLock) iterator.next();
148             if (lock.isOwnedByUser(user)) {
149                 delete(lock);
150             }
151         }
152     }
153 
154     /**
155      * @see org.kuali.rice.krad.service.PessimisticLockService#releaseAllLocksForUser(java.util.List, org.kuali.rice.kim.api.identity.Person, java.lang.String)
156      */
157     public void releaseAllLocksForUser(List<PessimisticLock> locks, Person user, String lockDescriptor) {
158         for (Iterator<PessimisticLock> iterator = locks.iterator(); iterator.hasNext();) {
159             PessimisticLock lock = (PessimisticLock) iterator.next();
160             if ( (lock.isOwnedByUser(user)) && (lockDescriptor.equals(lock.getLockDescriptor())) ) {
161                 delete(lock);
162             }
163         }
164     }
165 
166     /**
167      * @see org.kuali.rice.krad.service.PessimisticLockService#save(org.kuali.rice.krad.document.authorization.PessimisticLock)
168      */
169     public PessimisticLock save(PessimisticLock lock) {
170     	if ( LOG.isDebugEnabled() ) {
171     		LOG.debug("Saving lock: " + lock);
172     	}
173         return (PessimisticLock)getBusinessObjectService().save(lock);
174     }
175 
176     public BusinessObjectService getBusinessObjectService() {
177         return this.businessObjectService;
178     }
179 
180     public void setBusinessObjectService(BusinessObjectService businessObjectService) {
181         this.businessObjectService = businessObjectService;
182     }
183 
184     /**
185      * @param document
186      * @param user
187      * @return Set of actions are permitted the given user on the given document
188      */
189     public Set getDocumentActions(Document document, Person user, Set<String> documentActions){
190     	if(documentActions.contains(KRADConstants.KUALI_ACTION_CAN_CANCEL) && !hasPreRouteEditAuthorization(document, user) ){
191     		documentActions.remove(KRADConstants.KUALI_ACTION_CAN_CANCEL);
192     	}
193     	if(documentActions.contains(KRADConstants.KUALI_ACTION_CAN_SAVE)  && !hasPreRouteEditAuthorization(document, user)){
194     		documentActions.remove(KRADConstants.KUALI_ACTION_CAN_SAVE);
195     	}
196         if(documentActions.contains(KRADConstants.KUALI_ACTION_CAN_ROUTE) && !hasPreRouteEditAuthorization(document, user)){
197         	documentActions.remove(KRADConstants.KUALI_ACTION_CAN_ROUTE);
198         }
199         if (documentActions.contains(KRADConstants.KUALI_ACTION_CAN_BLANKET_APPROVE) && !hasPreRouteEditAuthorization(document, user)){
200         	documentActions.remove(KRADConstants.KUALI_ACTION_CAN_BLANKET_APPROVE);
201         }
202     	return documentActions;
203     }
204 
205 
206     /**
207      * This method checks to see that the given user has a lock on the document and return true if one is found.
208      *
209      * @param document - document to check
210      * @param user - current user
211      * @return true if the document is using Pessimistic Locking, the user has initiate authorization (see
212      *         {@link #hasInitiateAuthorization(Document, Person)}), and the document has a lock owned by the given
213      *         user. If the document is not using Pessimistic Locking the value returned will be that returned by
214      *         {@link #hasInitiateAuthorization(Document, Person)}.
215      */
216     protected boolean hasPreRouteEditAuthorization(Document document, Person user) {
217     	if (document.getPessimisticLocks().isEmpty()) {
218     		return true;
219     	}
220     	for (Iterator iterator = document.getPessimisticLocks().iterator(); iterator.hasNext();) {
221     		PessimisticLock lock = (PessimisticLock) iterator.next();
222     		if (lock.isOwnedByUser(user)) {
223     			return true;
224             }
225         }
226         return false;
227     }
228 
229 
230     protected boolean usesPessimisticLocking(Document document) {
231         return getDataDictionaryService().getDataDictionary().getDocumentEntry(document.getClass().getName()).getUsePessimisticLocking();
232     }
233 
234 
235     /**
236      * This method creates a new {@link PessimisticLock} when Workflow processing requires one
237      *
238      * @param document - the document to create the lock against and add the lock to
239      * @see org.kuali.rice.krad.document.authorization.DocumentAuthorizer#establishWorkflowPessimisticLocking(org.kuali.rice.krad.document.Document)
240      */
241     public void establishWorkflowPessimisticLocking(Document document) {
242         PessimisticLock lock = createNewPessimisticLock(document, new HashMap(), getWorkflowPessimisticLockOwnerUser());
243         document.addPessimisticLock(lock);
244     }
245 
246     /**
247      * This method releases locks created via the {@link #establishWorkflowPessimisticLocking(Document)} method for the given document
248      *
249      * @param document - document to release locks from
250      * @see org.kuali.rice.krad.document.authorization.DocumentAuthorizer#releaseWorkflowPessimisticLocking(org.kuali.rice.krad.document.Document)
251      */
252     public void releaseWorkflowPessimisticLocking(Document document) {
253         releaseAllLocksForUser(document.getPessimisticLocks(), getWorkflowPessimisticLockOwnerUser());
254         document.refreshPessimisticLocks();
255     }
256 
257     /**
258      * This method identifies the user that should be used to create and clear {@link PessimisticLock} objects required by
259      * Workflow.<br>
260      * <br>
261      * The default is the Kuali system user defined by {@link RiceConstants#SYSTEM_USER}. This method can be overriden by
262      * implementing documents if another user is needed.
263      *
264      * @return a valid {@link Person} object
265      */
266     protected Person getWorkflowPessimisticLockOwnerUser() {
267         String networkId = KRADConstants.SYSTEM_USER;
268         return getPersonService().getPersonByPrincipalName(networkId);
269     }
270 
271     /**
272      * This implementation will check the given document, editMode map, and user object to verify Pessimistic Locking. If the
273      * given edit mode map contains an 'entry type' edit mode then the system will check the locks already in existence on
274      * the document. If a valid lock for the given user is found the system will return the given edit mode map. If a valid
275      * lock is found but is owned by another user the edit mode map returned will have any 'entry type' edit modes removed. If the
276      * given document has no locks and the edit mode map passed in has at least one 'entry type' mode then a new
277      * {@link PessimisticLock} object will be created and set on the document for the given user.<br>
278      * <br>
279      * NOTE: This method is only called if the document uses pessimistic locking as described in the data dictionary file.
280      *
281      * @see org.kuali.rice.krad.document.authorization.DocumentAuthorizer#establishLocks(org.kuali.rice.krad.document.Document,
282      *      java.util.Map, org.kuali.rice.kim.api.identity.Person)
283      */
284     public Map establishLocks(Document document, Map editMode, Person user) {
285         Map editModeMap = new HashMap();
286         // givenUserLockDescriptors is a list of lock descriptors currently held on the document by the given user
287         List<String> givenUserLockDescriptors = new ArrayList<String>();
288         // lockDescriptorUsers is a map with lock descriptors as keys and users other than the given user who hold a lock of each descriptor
289         Map<String,Set<Person>> lockDescriptorUsers = new HashMap<String,Set<Person>>();
290 
291         // build the givenUserLockDescriptors set and the lockDescriptorUsers map
292         for (PessimisticLock lock : document.getPessimisticLocks()) {
293             if (lock.isOwnedByUser(user)) {
294                 // lock is owned by given user
295                 givenUserLockDescriptors.add(lock.getLockDescriptor());
296             } else {
297                 // lock is not owned by the given user
298                 if (!lockDescriptorUsers.containsKey(lock.getLockDescriptor())) {
299                     lockDescriptorUsers.put(lock.getLockDescriptor(), new HashSet<Person>());
300                 }
301                 ((Set<Person>) lockDescriptorUsers.get(lock.getLockDescriptor())).add(lock.getOwnedByUser());
302             }
303         }
304 
305         // verify that no locks held by current user exist for any other user
306         for (String givenUserLockDescriptor : givenUserLockDescriptors) {
307             if ( (lockDescriptorUsers.containsKey(givenUserLockDescriptor)) && (lockDescriptorUsers.get(givenUserLockDescriptor).size() > 0) ) {
308                 Set<Person> users = lockDescriptorUsers.get(givenUserLockDescriptor);
309                 if ( (users.size() != 1) || (!getWorkflowPessimisticLockOwnerUser().getPrincipalId().equals(users.iterator().next().getPrincipalId())) ) {
310                     String descriptorText = (document.useCustomLockDescriptors()) ? " using lock descriptor '" + givenUserLockDescriptor + "'" : "";
311                     String errorMsg = "Found an invalid lock status on document number " + document.getDocumentNumber() + "with current user and other user both having locks" + descriptorText + " concurrently";
312                     LOG.debug(errorMsg);
313                     throw new PessimisticLockingException(errorMsg);
314                 }
315             }
316         }
317 
318         // check to see if the given user has any locks in the system at all
319         if (givenUserLockDescriptors.isEmpty()) {
320             // given user has no locks... check for other user locks
321             if (lockDescriptorUsers.isEmpty()) {
322                 // no other user has any locks... set up locks for given user if user has edit privileges
323                 if (isLockRequiredByUser(document, editMode, user)) {
324                     document.addPessimisticLock(createNewPessimisticLock(document, editMode, user));
325                 }
326                 editModeMap.putAll(editMode);
327             } else {
328                 // at least one other user has at least one other lock... adjust edit mode for read only
329                 if (document.useCustomLockDescriptors()) {
330                     // check to see if the custom lock descriptor is already in use
331                     String customLockDescriptor = document.getCustomLockDescriptor(user);
332                     if (lockDescriptorUsers.containsKey(customLockDescriptor)) {
333                         // at least one other user has this descriptor locked... remove editable edit modes
334                         editModeMap = getEditModeWithEditableModesRemoved(editMode);
335                     } else {
336                         // no other user has a lock with this descriptor
337                         if (isLockRequiredByUser(document, editMode, user)) {
338                             document.addPessimisticLock(createNewPessimisticLock(document, editMode, user));
339                         }
340                         editModeMap.putAll(editMode);
341                     }
342                 } else {
343                     editModeMap = getEditModeWithEditableModesRemoved(editMode);
344                 }
345             }
346         } else {
347             // given user already has at least one lock descriptor
348             if (document.useCustomLockDescriptors()) {
349                 // get the custom lock descriptor and check to see if if the given user has a lock with that descriptor
350                 String customLockDescriptor = document.getCustomLockDescriptor(user);
351                 if (givenUserLockDescriptors.contains(customLockDescriptor)) {
352                     // user already has lock that is required
353                     editModeMap.putAll(editMode);
354                 } else {
355                     // user does not have lock for descriptor required
356                     if (lockDescriptorUsers.containsKey(customLockDescriptor)) {
357                         // another user has the lock descriptor that the given user requires... disallow lock and alter edit modes to have read only
358                         editModeMap = getEditModeWithEditableModesRemoved(editMode);
359                     } else {
360                         // no other user has a lock with this descriptor... check if this user needs a lock
361                         if (isLockRequiredByUser(document, editMode, user)) {
362                             document.addPessimisticLock(createNewPessimisticLock(document, editMode, user));
363                         }
364                         editModeMap.putAll(editMode);
365                     }
366                 }
367             } else {
368                 // user already has lock and no descriptors are being used... use the existing edit modes
369                 editModeMap.putAll(editMode);
370             }
371         }
372 
373         return editModeMap;
374     }
375 
376     /**
377      * This method is used to check if the given parameters warrant a new lock to be created for the given user. This method
378      * utilizes the {@link #isEntryEditMode(java.util.Map.Entry)} method.
379      *
380      * @param document -
381      *            document to verify lock creation against
382      * @param editMode -
383      *            edit modes list to check for 'entry type' edit modes
384      * @param user -
385      *            user the lock will be 'owned' by
386      * @return true if the given edit mode map has at least one 'entry type' edit mode... false otherwise
387      */
388     protected boolean isLockRequiredByUser(Document document, Map editMode, Person user) {
389         // check for entry edit mode
390         for (Iterator iterator = editMode.entrySet().iterator(); iterator.hasNext();) {
391             Map.Entry entry = (Map.Entry) iterator.next();
392             if (isEntryEditMode(entry)) {
393                 return true;
394             }
395         }
396         return false;
397     }
398 
399    /**
400      * This method is used to remove edit modes from the given map that allow the user to edit data on the document. This
401      * method utilizes the {@link #isEntryEditMode(java.util.Map.Entry)} method to identify if an edit mode is defined as an
402      * 'entry type' edit mode. It also uses the {@link #getEntryEditModeReplacementMode(java.util.Map.Entry)} method to replace
403      * any 'entry type' edit modes it finds.
404      *
405      * @param currentEditMode -
406      *            current set of edit modes the user has assigned to them
407      * @return an adjusted edit mode map where 'entry type' edit modes have been removed or replaced using the
408      *         {@link #getEntryEditModeReplacementMode()} method
409      */
410     protected Map getEditModeWithEditableModesRemoved(Map currentEditMode) {
411         Map editModeMap = new HashMap();
412         for (Iterator iterator = currentEditMode.entrySet().iterator(); iterator.hasNext();) {
413             Map.Entry entry = (Map.Entry) iterator.next();
414             if (isEntryEditMode(entry)) {
415                 editModeMap.putAll(getEntryEditModeReplacementMode(entry));
416             } else {
417                 editModeMap.put(entry.getKey(), entry.getValue());
418             }
419         }
420         return editModeMap;
421     }
422 
423     /**
424      * 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
425      * signify that this user has that edit mode available to them
426      *
427      * @param entry -
428      *            the {@link Map.Entry} object that contains an edit mode such as the ones returned but
429      *            {@link #getEditMode(Document, Person)}
430      * @return true if the given entry has a key signifying an 'entry type' edit mode and the value is equal to
431      *         {@link #EDIT_MODE_DEFAULT_TRUE_VALUE}... false if not
432      */
433     protected boolean isEntryEditMode(Map.Entry entry) {
434     	// check for FULL_ENTRY edit mode set to default true value
435     	if (AuthorizationConstants.EditMode.FULL_ENTRY.equals(entry.getKey())) {
436     		String fullEntryEditModeValue = (String)entry.getValue();           
437     		return ( StringUtils.equalsIgnoreCase(KRADConstants.KUALI_DEFAULT_TRUE_VALUE, fullEntryEditModeValue) );
438     	}
439     	return false;
440     }
441 
442     /**
443      * 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
444      *
445      * @param entry - the current 'entry type' edit mode to replace
446      * @return a Map of edit modes that will be used to replace this edit mode (represented by the given entry parameter)
447      */
448     protected Map getEntryEditModeReplacementMode(Map.Entry entry) {
449         Map editMode = new HashMap();
450         editMode.put(AuthorizationConstants.EditMode.VIEW_ONLY, KRADConstants.KUALI_DEFAULT_TRUE_VALUE);
451         return editMode;
452     }
453 
454     /**
455      * This method creates a new {@link PessimisticLock} object using the given document and user. If the document's
456      * useCustomLockDescriptors() method returns true then the new lock will also have a custom lock descriptor
457      * value set to the return value of the document's getCustomLockDescriptor(Person) method.
458      *
459      * @param document -
460      *            document to place the lock on
461      * @param editMode -
462      *            current edit modes for given user
463      * @param user -
464      *            user who will 'own' the new lock object
465      * @return the newly created lock object
466      */
467     protected PessimisticLock createNewPessimisticLock(Document document, Map editMode, Person user) {
468         if (document.useCustomLockDescriptors()) {
469             return generateNewLock(document.getDocumentNumber(), document.getCustomLockDescriptor(user), user);
470         } else {
471             return generateNewLock(document.getDocumentNumber(), user);
472         }
473     }
474 
475     public PersonService getPersonService() {
476         if ( personService == null ) {
477             personService = KimApiServiceLocator.getPersonService();
478         }
479         return personService;
480     }
481 
482 	public DataDictionaryService getDataDictionaryService() {
483         if ( dataDictionaryService == null ) {
484         	dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
485         }
486 		return dataDictionaryService;
487 	}
488 
489 	public PermissionService getPermissionService() {
490         if ( permissionService == null ) {
491         	permissionService = KimApiServiceLocator.getPermissionService();
492         }
493 		return permissionService;
494 	}
495 
496 
497 
498 }
499