/* * Copyright (C) 2005-2009 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have recieved a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.repo.domain.locks; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.alfresco.repo.domain.QNameDAO; import org.alfresco.repo.lock.LockAcquisitionException; import org.alfresco.service.namespace.QName; import org.alfresco.util.EqualsHelper; import org.springframework.dao.ConcurrencyFailureException; /** * Abstract implementation of the Locks DAO. * * @author Derek Hulley * @since 3.2 */ public abstract class AbstractLockDAOImpl implements LockDAO { private static final String LOCK_TOKEN_RELEASED = "not-locked"; private QNameDAO qnameDAO; /** * @return Returns the DAO for namespace ID resolution */ protected QNameDAO getQNameDAO() { return qnameDAO; } /** * @param qnameDAO DAO for namespace ID resolution */ public void setQnameDAO(QNameDAO qnameDAO) { this.qnameDAO = qnameDAO; } public void getLock(QName lockQName, String lockToken, long timeToLive) { String qnameNamespaceUri = lockQName.getNamespaceURI(); String qnameLocalName = lockQName.getLocalName(); // Force lower case for case insensitivity if (!qnameLocalName.toLowerCase().equals(qnameLocalName)) { lockQName = QName.createQName(qnameNamespaceUri, qnameLocalName.toLowerCase()); qnameLocalName = lockQName.getLocalName(); } // Force the lock token to lowercase lockToken = lockToken.toLowerCase(); // Resolve the namespace Long qnameNamespaceId = qnameDAO.getOrCreateNamespace(qnameNamespaceUri).getFirst(); // Get the lock resource for the exclusive lock. // All the locks that are created will need the exclusive case. LockResourceEntity exclusiveLockResource = getLockResource(qnameNamespaceId, qnameLocalName); if (exclusiveLockResource == null) { // Create it exclusiveLockResource = createLockResource(qnameNamespaceId, qnameLocalName); } Long requiredExclusiveLockResourceId = exclusiveLockResource.getId(); // Split the lock name List lockQNames = splitLockQName(lockQName); List requiredLockResourceIds = new ArrayList(lockQNames.size()); // Create the lock resources for (QName lockQNameIter : lockQNames) { String localname = lockQNameIter.getLocalName(); // Get the basic lock resource, forcing a create LockResourceEntity lockResource = getLockResource(qnameNamespaceId, localname); if (lockResource == null) { // Create it lockResource = createLockResource(qnameNamespaceId, localname); } requiredLockResourceIds.add(lockResource.getId()); } // Now, get all locks for the resources we will need List existingLocks = getLocksBySharedResourceIds(requiredLockResourceIds); Map existingLocksMap = new HashMap(); // Check them and make sure they don't prevent locks for (LockEntity existingLock : existingLocks) { boolean canTakeLock = canTakeLock(existingLock, lockToken, requiredExclusiveLockResourceId); if (!canTakeLock) { throw new LockAcquisitionException( LockAcquisitionException.ERR_EXCLUSIVE_LOCK_EXISTS, lockQName, lockToken); } existingLocksMap.put(existingLock, existingLock); } // Take the locks for the resources. // Existing locks must be updated, if required. for (Long requiredLockResourceId : requiredLockResourceIds) { LockEntity requiredLock = new LockEntity(); requiredLock.setSharedResourceId(requiredLockResourceId); requiredLock.setExclusiveResourceId(requiredExclusiveLockResourceId); // Does it exist? if (existingLocksMap.containsKey(requiredLock)) { requiredLock = existingLocksMap.get(requiredLock); // Do an update updateLock(requiredLock, lockToken, timeToLive); } else { // Create it requiredLock = createLock( requiredLockResourceId, requiredExclusiveLockResourceId, lockToken, timeToLive); } } // Done } public void refreshLock(QName lockQName, String lockToken, long timeToLive) { updateLocks(lockQName, lockToken, lockToken, timeToLive); } public void releaseLock(QName lockQName, String lockToken) { updateLocks(lockQName, lockToken, LOCK_TOKEN_RELEASED, 0L); } /** * Put new values against the given exclusive lock. This works against the related locks as * well. * @throws LockAcquisitionException on failure */ private void updateLocks(QName lockQName, String lockToken, String newLockToken, long timeToLive) { String qnameNamespaceUri = lockQName.getNamespaceURI(); String qnameLocalName = lockQName.getLocalName(); // Force lower case for case insensitivity if (!qnameLocalName.toLowerCase().equals(qnameLocalName)) { lockQName = QName.createQName(qnameNamespaceUri, qnameLocalName.toLowerCase()); qnameLocalName = lockQName.getLocalName(); } // Force the lock token to lowercase lockToken = lockToken.toLowerCase(); // Resolve the namespace Long qnameNamespaceId = qnameDAO.getOrCreateNamespace(qnameNamespaceUri).getFirst(); // Get the lock resource for the exclusive lock. // All the locks that are created will need the exclusive case. LockResourceEntity exclusiveLockResource = getLockResource(qnameNamespaceId, qnameLocalName); if (exclusiveLockResource == null) { // If the exclusive lock doesn't exist, the locks don't exist throw new LockAcquisitionException( LockAcquisitionException.ERR_LOCK_RESOURCE_MISSING, lockQName, lockToken); } Long exclusiveLockResourceId = exclusiveLockResource.getId(); // Split the lock name List lockQNames = splitLockQName(lockQName); // We just need to know how many resources needed updating. // They will all share the same exclusive lock resource int requiredUpdateCount = lockQNames.size(); // Update int updateCount = updateLocks(exclusiveLockResourceId, lockToken, newLockToken, timeToLive); // Check if (updateCount != requiredUpdateCount) { throw new LockAcquisitionException( LockAcquisitionException.ERR_LOCK_UPDATE_COUNT, lockQName, lockToken, new Integer(updateCount), new Integer(requiredUpdateCount)); } // Done } /** * Validate if a lock can be taken or not. */ private boolean canTakeLock(LockEntity existingLock, String lockToken, Long desiredExclusiveLock) { if (EqualsHelper.nullSafeEquals(existingLock.getLockToken(), lockToken)) { // The lock token is the same. // Regardless of lock expiry, the lock can be taken return true; } else if (existingLock.hasExpired()) { // Expired locks are fair game return true; } else if (existingLock.isExclusive()) { // It's a valid, exclusive lock held using a different token ... return false; } else if (desiredExclusiveLock.equals(existingLock.getSharedResourceId())) { // We can't take an exclusive lock if a shared lock is active return false; } else { // Good to go return true; } } /** * Override to get the unique, lock resource entity if one exists. * * @param qnameNamespaceId the namespace entity ID * @param qnameLocalName the lock localname * @return Returns the lock resource entity, * or null if it doesn't exist */ protected abstract LockResourceEntity getLockResource(Long qnameNamespaceId, String qnameLocalName); /** * Create a unique lock resource * * @param qnameNamespaceId the namespace entity ID * @param qnameLocalName the lock localname * @return Returns the newly created lock resource entity */ protected abstract LockResourceEntity createLockResource(Long qnameNamespaceId, String qnameLocalName); /** * @param id the lock instance ID * @return Returns the lock, if it exists, otherwise null */ protected abstract LockEntity getLock(Long id); /** * @param sharedResourceId the shared lock resource ID * @param exclusiveResourceId the exclusive lock resource ID * @return Returns the lock, if it exists, otherwise null */ protected abstract LockEntity getLock(Long sharedResourceId, Long exclusiveResourceId); /** * Get any existing lock data for the shared resources. The locks returned are not filtered and * may be expired. * * @param lockResourceIds a list of shared resource IDs for which to retrieve the current locks * @return Returns a list of locks (expired or not) for the given lock resources */ protected abstract List getLocksBySharedResourceIds(List sharedLockResourceIds); /** * Create a new lock. * @param sharedResourceId the specific resource to lock * @param exclusiveResourceId the exclusive lock that is being sought * @param lockToken the lock token to assign * @param timeToLive the time, in milliseconds, for the lock to remain valid * @return Returns the new lock * @throws ConcurrencyFailureException if the lock was already taken at the time of creation */ protected abstract LockEntity createLock( Long sharedResourceId, Long exclusiveResourceId, String lockToken, long timeToLive); /** * Update an existing lock * @param lockEntity the specific lock to update * @param lockApplicant the new lock token * @param timeToLive the new lock time, in milliseconds, for the lock to remain valid * @return Returns the updated lock */ protected abstract LockEntity updateLock( LockEntity lockEntity, String lockToken, long timeToLive); /** * @param exclusiveLockResourceId the exclusive resource ID being locks * @param oldLockToken the lock token to change from * @param newLockToken the new lock token * @param timeToLive the new time to live (in milliseconds) * @return the number of rows updated */ protected abstract int updateLocks( Long exclusiveLockResourceId, String oldLockToken, String newLockToken, long timeToLive); /** * Split a lock's qualified name into the component parts using the '.' (period) as a * separator on the localname. The namespace is preserved. The provided qualified * name will always be the last component in the returned list. * * @param lockQName the lock name to split into it's higher-level paths * @return Returns the namespace ID along with the ordered localnames */ protected List splitLockQName(QName lockQName) { String ns = lockQName.getNamespaceURI(); String name = lockQName.getLocalName(); StringTokenizer tokenizer = new StringTokenizer(name, "."); List ret = new ArrayList(tokenizer.countTokens()); StringBuilder sb = new StringBuilder(); // Fill it boolean first = true; while (tokenizer.hasMoreTokens()) { if (first) { first = false; } else { sb.append("."); } sb.append(tokenizer.nextToken()); QName parentLockQName = QName.createQName(ns, sb.toString()); ret.add(parentLockQName); } // Done return ret; } }