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