/**
* 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;
}
}