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