/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of the License at the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package org.apereo.portal.concurrency.locking; import java.util.Date; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apereo.portal.EntityIdentifier; import org.apereo.portal.concurrency.IEntityLock; import org.apereo.portal.concurrency.IEntityLockService; import org.apereo.portal.concurrency.LockingException; import org.apereo.portal.properties.PropertiesManager; public class ReferenceEntityLockService implements IEntityLockService { private static final Log log = LogFactory.getLog(ReferenceEntityLockService.class); // Singleton instance: private static IEntityLockService singleton = null; // Store for IEntityLocks: private IEntityLockStore lockStore = null; // Locking properties, initialized with default values, are settable // via portal.properties: // Are we running in a multi-server environment? If so, the lock store // will be in persistent storage. private boolean multiServer = false; // Lifetime of a lock in seconds, defaults to 5 minutes. private int defaultLockPeriod = 300; /* Fudge factor in milliseconds, extends the apparent expiration times * of potentially conflicting locks beyond their actual expirations. * We only use it when checking for locking conflicts and then only if * inMemory == false. Defaults to 5000. */ private int lockToleranceMillis = 5000; /** ReferenceEntityLockingService constructor comment. */ public ReferenceEntityLockService() throws LockingException { super(); initialize(); } /** * Attempts to change the lock's <code>lockType</code> to <code>newType</code>. * * @param lock IEntityLock * @param newType int * @exception org.apereo.portal.concurrency.LockingException */ public void convert(IEntityLock lock, int newType) throws LockingException { convert(lock, newType, defaultLockPeriod); } /** * Attempts to change the lock's <code>lockType</code> to <code>newType</code>. * * @param lock IEntityLock * @param newType int * @param newDuration int * @exception org.apereo.portal.concurrency.LockingException */ public void convert(IEntityLock lock, int newType, int newDuration) throws LockingException { if (lock.getLockType() == newType) { throw new LockingException( "Could not convert " + lock + " : old and new lock TYPEs are the same."); } if (!isValidLockType(newType)) { throw new LockingException( "Could not convert " + lock + " : lock TYPE " + newType + " is invalid."); } if (!isValid(lock)) { throw new LockingException("Could not convert " + lock + " : lock is invalid."); } if (newType == WRITE_LOCK && retrieveLocks(lock.getEntityType(), lock.getEntityKey(), null).length > 1) { throw new LockingException( "Could not convert " + lock + " : another lock already exists."); } if (newType == READ_LOCK) { /* Can always convert to READ */ } Date newExpiration = getNewExpiration(newDuration); getLockStore().update(lock, newExpiration, new Integer(newType)); ((EntityLockImpl) lock).setLockType(newType); ((EntityLockImpl) lock).setExpirationTime(newExpiration); } /** * Answer if this <code>IEntityLock</code> exists in the store. * * @param lock * @return boolean */ public boolean existsInStore(IEntityLock lock) throws LockingException { Class entityType = lock.getEntityType(); String key = lock.getEntityKey(); Integer lockType = new Integer(lock.getLockType()); Date expiration = lock.getExpirationTime(); String owner = lock.getLockOwner(); IEntityLock[] lockArray = getLockStore().find(entityType, key, lockType, expiration, owner); return (lockArray.length > 0); } /** @return int */ private int getDefaultLockPeriod() { return defaultLockPeriod; } /** @return org.apereo.portal.concurrency.locking.IEntityLockStore */ private IEntityLockStore getLockStore() { return lockStore; } /** @return int */ private int getLockToleranceMillis() { return lockToleranceMillis; } /** @return java.util.Date */ private Date getNewExpiration(int durationSecs) { return new Date(System.currentTimeMillis() + (durationSecs * 1000)); } /** @exception LockingException */ private void initialize() throws LockingException { String eMsg = null; try { multiServer = PropertiesManager.getPropertyAsBoolean( "org.apereo.portal.concurrency.multiServer", false); lockStore = (multiServer) ? RDBMEntityLockStore.singleton() : MemoryEntityLockStore.singleton(); } catch (Exception e) { eMsg = "ReferenceEntityLockingService.initialize(): Failed to instantiate entity lock store. " + e; log.error(eMsg); throw new LockingException(eMsg); } try { int lockDuration = PropertiesManager.getPropertyAsInt( "org.apereo.portal.concurrency.IEntityLockService.defaultLockDuration"); setDefaultLockPeriod(lockDuration); } catch (Exception ex) { /* defaults to 5 minutes. */ } if (multiServer) { try { int lockTolerance = PropertiesManager.getPropertyAsInt( "org.apereo.portal.concurrency.clockTolerance"); setLockToleranceMillis(lockTolerance); } catch (Exception ex) { /* defaults to 0. */ } } } /** * Answers if the entity represented by the entityType and entityKey already has a lock of some * type. * * @param entityType * @param entityKey * @exception org.apereo.portal.concurrency.LockingException */ private boolean isLocked(Class entityType, String entityKey) throws LockingException { return isLocked(entityType, entityKey, null); } /** * Answers if the entity represented by entityType and entityKey has one or more locks. Param * <code>lockType</code> can be null. * * @param entityType * @param entityKey * @param lockType (optional) * @exception org.apereo.portal.concurrency.LockingException */ private boolean isLocked(Class entityType, String entityKey, Integer lockType) throws LockingException { IEntityLock[] locks = retrieveLocks(entityType, entityKey, lockType); return locks.length > 0; } /** @return boolean */ private boolean isMultiServer() { return multiServer; } /** * @param lock IEntityLock * @return boolean */ private boolean isUnexpired(IEntityLock lock) { return lock.getExpirationTime().getTime() > System.currentTimeMillis(); } /** * Answers if this <code>IEntityLock</code> represents a lock that is still good. To be valid, a * lock must exist in the underlying store and be unexpired. * * @param lock IEntityLock * @exception org.apereo.portal.concurrency.LockingException */ public boolean isValid(IEntityLock lock) throws LockingException { return isUnexpired(lock) && existsInStore(lock); } private boolean isValidLockType(int lockType) { return ((lockType == READ_LOCK) || (lockType == WRITE_LOCK)); } /** * Returns a lock for the entity, lock type and owner if no conflicting locks exist. * * @param entityType * @param entityKey * @param lockType * @param owner * @return org.apereo.portal.groups.IEntityLock * @exception LockingException */ public IEntityLock newLock(Class entityType, String entityKey, int lockType, String owner) throws LockingException { return newLock(entityType, entityKey, lockType, owner, defaultLockPeriod); } /** * Returns a lock for the entity, lock type and owner if no conflicting locks exist. * * @param entityType * @param entityKey * @param lockType * @param owner * @param durationSecs * @return org.apereo.portal.groups.IEntityLock * @exception LockingException * <p>Retrieves potentially conflicting locks and checks them before adding the new lock to * the store. The add of a write lock will fail if any other lock exists for the entity. The * add of a read lock will fail if a write lock exists for the entity. After we add a write * lock we check the store a second time and roll back if any other lock has snuck in. I * think this is slightly safer than depending on the db isolation level for transactional * integrity. */ public IEntityLock newLock( Class entityType, String entityKey, int lockType, String owner, int durationSecs) throws LockingException { int expirationSecs = durationSecs; Date expires = getNewExpiration(expirationSecs); IEntityLock newLock = new EntityLockImpl(entityType, entityKey, lockType, expires, owner, this); // retrieve potentially conflicting locks: IEntityLock[] locks = retrieveLocks(entityType, entityKey, null); if (lockType == WRITE_LOCK) { if (locks.length > 0) { throw new LockingException("Could not create lock: entity already locked."); } getLockStore().add(newLock); locks = retrieveLocks(entityType, entityKey, null); if (locks.length > 1) // another lock snuck in { release(newLock); throw new LockingException("Could not create lock: entity already locked."); } } else // ( lockType == READ_LOCK ) { for (int i = 0; i < locks.length; i++) { if (locks[i].getLockType() == WRITE_LOCK) { throw new LockingException( "Could not create lock: entity already write locked."); } else { if (locks[i].equals(newLock)) { // another read lock from the same owner; bump the expiration time. expirationSecs++; expires = getNewExpiration(expirationSecs); newLock = new EntityLockImpl( entityType, entityKey, lockType, expires, owner, this); } } } getLockStore().add(newLock); } return newLock; } /** * Returns a lock for the entity, lock type and owner if no conflicting locks exist. * * @return org.apereo.portal.groups.IEntityLock * @param entityID org.apereo.portal.EntityIdentifier * @param lockType int * @param owner String * @exception LockingException */ public IEntityLock newLock(EntityIdentifier entityID, int lockType, String owner) throws LockingException { return newLock(entityID.getType(), entityID.getKey(), lockType, owner, defaultLockPeriod); } /** * Returns a lock for the entity, lock type and owner if no conflicting locks exist. * * @return org.apereo.portal.groups.IEntityLock * @param entityID org.apereo.portal.EntityIdentifier * @param lockType int * @param owner String * @param durationSecs int * @exception LockingException */ public IEntityLock newLock( EntityIdentifier entityID, int lockType, String owner, int durationSecs) throws LockingException { return newLock(entityID.getType(), entityID.getKey(), lockType, owner, durationSecs); } /** * Releases the <code>IEntityLock</code>. * * @param lock IEntityLock * @exception LockingException */ public void release(IEntityLock lock) throws LockingException { getLockStore().delete(lock); ((EntityLockImpl) lock).setExpirationTime(new Date(0)); } /** * Extends the expiration time of the lock by some service-defined increment. * * @param lock IEntityLock * @exception LockingException */ public void renew(IEntityLock lock) throws LockingException { renew(lock, defaultLockPeriod); } /** * Extends the expiration time of the lock by some service-defined increment. * * @param lock IEntityLock * @exception LockingException */ public void renew(IEntityLock lock, int duration) throws LockingException { if (isValid(lock)) { Date newExpiration = getNewExpiration(duration); getLockStore().update(lock, newExpiration); ((EntityLockImpl) lock).setExpirationTime(newExpiration); } else { throw new LockingException("Could not renew " + lock + " : lock is invalid."); } } /** * Returns an IEntityLock[] containing unexpired locks for the entityType, entityKey and * lockType. Param <code>lockType</code> can be null. * * @param entityType * @param entityKey * @param lockType (optional) * @exception LockingException */ private IEntityLock[] retrieveLocks(Class entityType, String entityKey, Integer lockType) throws LockingException { Date expiration = (multiServer) ? new Date(System.currentTimeMillis() - getLockToleranceMillis()) : new Date(); return getLockStore().findUnexpired(expiration, entityType, entityKey, lockType, null); } /** @param newDefaultLockPeriod int */ private void setDefaultLockPeriod(int newDefaultLockPeriod) { defaultLockPeriod = newDefaultLockPeriod; } /** @param newLockToleranceMillis int */ private void setLockToleranceMillis(int newLockToleranceMillis) { lockToleranceMillis = newLockToleranceMillis; } /** @param newMultiServer boolean */ private void setMultiServer(boolean newMultiServer) { multiServer = newMultiServer; } /** @return org.apereo.portal.concurrency.locking.ReferenceEntityLockService */ public static synchronized IEntityLockService singleton() throws LockingException { if (singleton == null) { singleton = new ReferenceEntityLockService(); } return singleton; } }