/**
* 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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apereo.portal.concurrency.IEntityLock;
import org.apereo.portal.concurrency.LockingException;
import org.apereo.portal.jdbc.RDBMServices;
import org.apereo.portal.spring.locator.EntityTypesLocator;
/**
* RDBMS-based store for <code>IEntityLocks</code>.
*
*/
public class RDBMEntityLockStore implements IEntityLockStore {
private static final Log log = LogFactory.getLog(RDBMEntityLockStore.class);
private static IEntityLockStore singleton;
// Constants for the LOCK table:
private static String LOCK_TABLE = "UP_ENTITY_LOCK";
private static String ENTITY_TYPE_COLUMN = "ENTITY_TYPE_ID";
private static String ENTITY_KEY_COLUMN = "ENTITY_KEY";
private static String EXPIRATION_TIME_COLUMN = "EXPIRATION_TIME";
private static String LOCK_OWNER_COLUMN = "LOCK_OWNER";
private static String LOCK_TYPE_COLUMN = "LOCK_TYPE";
private static String EQ = " = ";
private static String GT = " > ";
private static String LT = " < ";
private static String QUOTE = "'";
private static String allLockColumns;
private static String addSql;
private static String deleteLockSql;
private static String updateSql;
// Prior to jdk 1.4, java.sql.Timestamp.getTime() truncated milliseconds.
private static boolean timestampHasMillis;
static {
Date testDate = new Date();
Timestamp testTimestamp = new Timestamp(testDate.getTime());
timestampHasMillis = (testDate.getTime() == testTimestamp.getTime());
}
/** RDBMEntityGroupStore constructor. */
public RDBMEntityLockStore() throws LockingException {
super();
initialize();
}
/**
* Adds the lock to the underlying store.
*
* @param lock
*/
public void add(IEntityLock lock) throws LockingException {
Connection conn = null;
try {
conn = RDBMServices.getConnection();
primDeleteExpired(new Date(), lock.getEntityType(), lock.getEntityKey(), conn);
primAdd(lock, conn);
} catch (SQLException sqle) {
throw new LockingException("Problem creating " + lock, sqle);
} finally {
RDBMServices.releaseConnection(conn);
}
}
/**
* If this IEntityLock exists, delete it.
*
* @param lock
*/
public void delete(IEntityLock lock) throws LockingException {
Connection conn = null;
try {
conn = RDBMServices.getConnection();
primDelete(lock, conn);
} catch (SQLException sqle) {
throw new LockingException("Problem deleting " + lock, sqle);
} finally {
RDBMServices.releaseConnection(conn);
}
}
/** Delete all IEntityLocks from the underlying store. */
public void deleteAll() throws LockingException {
Connection conn = null;
Statement stmnt = null;
try {
String sql = "DELETE FROM " + LOCK_TABLE;
if (log.isDebugEnabled()) log.debug("RDBMEntityLockStore.deleteAll(): " + sql);
conn = RDBMServices.getConnection();
try {
stmnt = conn.createStatement();
int rc = stmnt.executeUpdate(sql);
if (log.isDebugEnabled()) {
String msg = "Deleted " + rc + " locks.";
log.debug("RDBMEntityLockStore.deleteAll(): " + msg);
}
} finally {
if (stmnt != null) stmnt.close();
}
} catch (SQLException sqle) {
throw new LockingException("Problem deleting locks", sqle);
} finally {
RDBMServices.releaseConnection(conn);
}
}
/**
* Delete all expired IEntityLocks from the underlying store.
*
* @param expiration
*/
public void deleteExpired(Date expiration) throws LockingException {
deleteExpired(expiration, null, null);
}
/**
* Delete IEntityLocks from the underlying store that have expired as of <code>expiration</code>
* . Params <code>entityType</code> and <code>entityKey</code> are optional.
*
* @param expiration java.util.Date
* @param entityType Class
* @param entityKey String
*/
public void deleteExpired(Date expiration, Class entityType, String entityKey)
throws LockingException {
Connection conn = null;
try {
conn = RDBMServices.getConnection();
primDeleteExpired(expiration, entityType, entityKey, conn);
} catch (SQLException sqle) {
throw new LockingException("Problem deleting expired locks", sqle);
} finally {
RDBMServices.releaseConnection(conn);
}
}
/**
* Delete all expired IEntityLocks from the underlying store.
*
* @param lock IEntityLock
*/
public void deleteExpired(IEntityLock lock) throws LockingException {
deleteExpired(new Date(), lock.getEntityType(), lock.getEntityKey());
}
/**
* Retrieve IEntityLocks from the underlying store. Any or all of the parameters may be null.
*
* @param entityType Class
* @param entityKey String
* @param lockType Integer - so we can accept a null value.
* @param expiration Date
* @param lockOwner String
* @exception LockingException - wraps an Exception specific to the store.
*/
public IEntityLock[] find(
Class entityType, String entityKey, Integer lockType, Date expiration, String lockOwner)
throws LockingException {
return select(entityType, entityKey, lockType, expiration, lockOwner);
}
/**
* Retrieve IEntityLocks from the underlying store. Expiration must not be null.
*
* @param expiration Date
* @param entityType Class
* @param entityKey String
* @param lockType Integer - so we can accept a null value.
* @param lockOwner String
* @exception LockingException - wraps an Exception specific to the store.
*/
public IEntityLock[] findUnexpired(
Date expiration, Class entityType, String entityKey, Integer lockType, String lockOwner)
throws LockingException {
Timestamp ts = new Timestamp(expiration.getTime());
return selectUnexpired(ts, entityType, entityKey, lockType, lockOwner);
}
/** SQL for inserting a row into the lock table. */
private static String getAddSql() {
if (addSql == null) {
addSql =
"INSERT INTO "
+ LOCK_TABLE
+ "("
+ getAllLockColumns()
+ ") VALUES (?, ?, ?, ?, ?)";
}
return addSql;
}
/** @return java.lang.String */
private static java.lang.String getAllLockColumns() {
if (allLockColumns == null) {
StringBuffer buff = new StringBuffer(100);
buff.append(ENTITY_TYPE_COLUMN);
buff.append(", ");
buff.append(ENTITY_KEY_COLUMN);
buff.append(", ");
buff.append(LOCK_TYPE_COLUMN);
buff.append(", ");
buff.append(EXPIRATION_TIME_COLUMN);
buff.append(", ");
buff.append(LOCK_OWNER_COLUMN);
allLockColumns = buff.toString();
}
return allLockColumns;
}
/** SQL for deleting a row on the lock table. */
private static String getDeleteLockSql() {
if (deleteLockSql == null) {
deleteLockSql =
"DELETE FROM "
+ LOCK_TABLE
+ " WHERE "
+ ENTITY_TYPE_COLUMN
+ EQ
+ "?"
+ " AND "
+ ENTITY_KEY_COLUMN
+ EQ
+ "?"
+ " AND "
+ EXPIRATION_TIME_COLUMN
+ EQ
+ "?"
+ " AND "
+ LOCK_TYPE_COLUMN
+ EQ
+ "?"
+ " AND "
+ LOCK_OWNER_COLUMN
+ EQ
+ "?";
}
return deleteLockSql;
}
/** @return java.lang.String */
private static java.lang.String getSelectSql() {
return ("SELECT " + getAllLockColumns() + " FROM " + LOCK_TABLE);
}
/** SQL for updating a row on the lock table. */
private static String getUpdateSql() {
if (updateSql == null) {
updateSql =
"UPDATE "
+ LOCK_TABLE
+ " SET "
+ EXPIRATION_TIME_COLUMN
+ EQ
+ "?, "
+ LOCK_TYPE_COLUMN
+ EQ
+ "?"
+ " WHERE "
+ ENTITY_TYPE_COLUMN
+ EQ
+ "?"
+ " AND "
+ ENTITY_KEY_COLUMN
+ EQ
+ "?"
+ " AND "
+ LOCK_OWNER_COLUMN
+ EQ
+ "?"
+ " AND "
+ EXPIRATION_TIME_COLUMN
+ EQ
+ "?"
+ " AND "
+ LOCK_TYPE_COLUMN
+ EQ
+ "?";
}
return updateSql;
}
/** Cleanup the store by deleting locks expired an hour ago. */
private void initialize() throws LockingException {
Date expiration = new Date(System.currentTimeMillis() - (60 * 60 * 1000));
deleteExpired(expiration, null, null);
}
/**
* Extract values from ResultSet and create a new lock.
*
* @return org.apereo.portal.groups.IEntityLock
* @param rs java.sql.ResultSet
*/
private IEntityLock instanceFromResultSet(java.sql.ResultSet rs)
throws SQLException, LockingException {
Integer entityTypeID = new Integer(rs.getInt(1));
Class entityType = EntityTypesLocator.getEntityTypes().getEntityTypeFromID(entityTypeID);
String key = rs.getString(2);
int lockType = rs.getInt(3);
Timestamp ts = rs.getTimestamp(4);
String lockOwner = rs.getString(5);
return newInstance(entityType, key, lockType, ts, lockOwner);
}
/** @return org.apereo.portal.concurrency.locking.IEntityLock */
private IEntityLock newInstance(
Class entityType, String entityKey, int lockType, Date expirationTime, String lockOwner)
throws LockingException {
return new EntityLockImpl(entityType, entityKey, lockType, expirationTime, lockOwner);
}
/**
* Add the lock to the underlying store.
*
* @param lock org.apereo.portal.concurrency.locking.IEntityLock
* @param conn java.sql.Connection
*/
private void primAdd(IEntityLock lock, Connection conn) throws SQLException, LockingException {
Integer typeID =
EntityTypesLocator.getEntityTypes().getEntityIDFromType(lock.getEntityType());
String key = lock.getEntityKey();
int lockType = lock.getLockType();
Timestamp ts = new Timestamp(lock.getExpirationTime().getTime());
String owner = lock.getLockOwner();
try {
PreparedStatement ps = conn.prepareStatement(getAddSql());
try {
ps.setInt(1, typeID.intValue()); // entity type
ps.setString(2, key); // entity key
ps.setInt(3, lockType); // lock type
ps.setTimestamp(4, ts); // lock expiration
ps.setString(5, owner); // lock owner
if (log.isDebugEnabled()) log.debug("RDBMEntityLockStore.primAdd(): " + ps);
int rc = ps.executeUpdate();
if (rc != 1) {
String errString = "Problem adding " + lock;
log.error(errString);
throw new LockingException(errString);
}
} finally {
if (ps != null) ps.close();
}
} catch (java.sql.SQLException sqle) {
log.error(sqle, sqle);
throw sqle;
}
}
/**
* Delete the IEntityLock from the underlying store.
*
* @param lock
* @param conn the database connection
*/
private void primDelete(IEntityLock lock, Connection conn)
throws LockingException, SQLException {
Integer typeID =
EntityTypesLocator.getEntityTypes().getEntityIDFromType(lock.getEntityType());
String key = lock.getEntityKey();
int lockType = lock.getLockType();
Timestamp ts = new Timestamp(lock.getExpirationTime().getTime());
String owner = lock.getLockOwner();
try {
PreparedStatement ps = conn.prepareStatement(getDeleteLockSql());
try {
ps.setInt(1, typeID.intValue()); // entity type
ps.setString(2, key); // entity key
ps.setTimestamp(3, ts); // lock expiration
ps.setInt(4, lockType); // lock type
ps.setString(5, owner); // lock owner
if (log.isDebugEnabled()) log.debug("RDBMEntityLockStore.primDelete(): " + ps);
int rc = ps.executeUpdate();
if (log.isDebugEnabled())
log.debug("RDBMEntityLockStore.primDelete(): deleted " + rc + " lock(s).");
} finally {
if (ps != null) ps.close();
}
} catch (java.sql.SQLException sqle) {
log.error(sqle, sqle);
throw sqle;
}
}
/**
* Delete IEntityLocks from the underlying store that have expired as of <code>expiration</code>
* . Params <code>entityType</code> and <code>entityKey</code> are optional.
*
* @param expiration java.util.Date
* @param entityType Class
* @param entityKey String
* @param conn Connection
*/
private void primDeleteExpired(
Date expiration, Class entityType, String entityKey, Connection conn)
throws LockingException, SQLException {
Statement stmnt = null;
Timestamp ts = new Timestamp(expiration.getTime());
StringBuffer buff = new StringBuffer(100);
buff.append("DELETE FROM " + LOCK_TABLE + " WHERE " + EXPIRATION_TIME_COLUMN + LT);
buff.append(printTimestamp(ts));
if (entityType != null) {
Integer typeID = EntityTypesLocator.getEntityTypes().getEntityIDFromType(entityType);
buff.append(" AND " + ENTITY_TYPE_COLUMN + EQ + typeID);
}
if (entityKey != null) {
buff.append(" AND " + ENTITY_KEY_COLUMN + EQ + sqlQuote(entityKey));
}
String sql = buff.toString();
if (log.isDebugEnabled()) log.debug("RDBMEntityLockStore.deleteExpired(): " + sql);
try {
stmnt = conn.createStatement();
int rc = stmnt.executeUpdate(sql);
if (log.isDebugEnabled()) {
String msg = "Deleted " + rc + " expired locks.";
log.debug("RDBMEntityLockStore.deleteExpired(): " + msg);
}
} catch (SQLException sqle) {
throw new LockingException("Problem deleting expired locks", sqle);
} finally {
if (stmnt != null) stmnt.close();
}
}
/**
* Retrieve IEntityLocks from the underlying store.
*
* @param sql String - the sql string used to select the entity lock rows.
* @exception LockingException - wraps an Exception specific to the store.
*/
private IEntityLock[] primSelect(String sql) throws LockingException {
Connection conn = null;
Statement stmnt = null;
ResultSet rs = null;
List locks = new ArrayList();
if (log.isDebugEnabled()) log.debug("RDBMEntityLockStore.primSelect(): " + sql);
try {
conn = RDBMServices.getConnection();
stmnt = conn.createStatement();
try {
rs = stmnt.executeQuery(sql);
try {
while (rs.next()) {
locks.add(instanceFromResultSet(rs));
}
} finally {
rs.close();
}
} finally {
stmnt.close();
}
} catch (SQLException sqle) {
log.error(sqle, sqle);
throw new LockingException("Problem retrieving EntityLocks", sqle);
} finally {
RDBMServices.releaseConnection(conn);
}
return ((IEntityLock[]) locks.toArray(new IEntityLock[locks.size()]));
}
/**
* Updates the lock's <code>expiration</code> and <code>lockType</code> in the underlying store.
* The SQL is over-qualified to make sure the row has not been updated since the lock was last
* checked.
*
* @param lock
* @param newExpiration java.util.Date
* @param newType Integer
* @param conn Connection
*/
private void primUpdate(IEntityLock lock, Date newExpiration, Integer newType, Connection conn)
throws SQLException, LockingException {
Integer typeID =
EntityTypesLocator.getEntityTypes().getEntityIDFromType(lock.getEntityType());
String key = lock.getEntityKey();
int oldLockType = lock.getLockType();
int newLockType = (newType == null) ? oldLockType : newType.intValue();
java.sql.Timestamp oldTs = new java.sql.Timestamp(lock.getExpirationTime().getTime());
java.sql.Timestamp newTs = new java.sql.Timestamp(newExpiration.getTime());
String owner = lock.getLockOwner();
try {
PreparedStatement ps = conn.prepareStatement(getUpdateSql());
try {
ps.setTimestamp(1, newTs); // new expiration
ps.setInt(2, newLockType); // new lock type
ps.setInt(3, typeID.intValue()); // entity type
ps.setString(4, key); // entity key
ps.setString(5, owner); // lock owner
ps.setTimestamp(6, oldTs); // old expiration
ps.setInt(7, oldLockType); // old lock type;
if (log.isDebugEnabled()) log.debug("RDBMEntityLockStore.primUpdate(): " + ps);
int rc = ps.executeUpdate();
if (rc != 1) {
String errString = "Problem updating " + lock;
log.error(errString);
throw new LockingException(errString);
}
} finally {
if (ps != null) ps.close();
}
} catch (java.sql.SQLException sqle) {
log.error(sqle, sqle);
throw sqle;
}
}
/**
* Retrieve IEntityLocks from the underlying store. Any or all of the parameters may be null.
*
* @param entityType Class
* @param entityKey String
* @param lockType Integer - so we can accept a null value.
* @param expiration Date
* @param lockOwner String
* @exception LockingException - wraps an Exception specific to the store.
*/
private IEntityLock[] select(
Class entityType, String entityKey, Integer lockType, Date expiration, String lockOwner)
throws LockingException {
StringBuffer sqlQuery = new StringBuffer(getSelectSql() + " WHERE 1 = 1");
if (entityType != null) {
Integer typeID = EntityTypesLocator.getEntityTypes().getEntityIDFromType(entityType);
sqlQuery.append(" AND " + ENTITY_TYPE_COLUMN + EQ + typeID);
}
if (entityKey != null) {
sqlQuery.append(" AND " + ENTITY_KEY_COLUMN + EQ + sqlQuote(entityKey));
}
if (lockType != null) {
sqlQuery.append(" AND " + LOCK_TYPE_COLUMN + EQ + lockType);
}
if (expiration != null) {
Timestamp ts = new Timestamp(expiration.getTime());
sqlQuery.append(" AND " + EXPIRATION_TIME_COLUMN + EQ + printTimestamp(ts));
}
if (lockOwner != null) {
sqlQuery.append(" AND " + LOCK_OWNER_COLUMN + EQ + sqlQuote(lockOwner));
}
return primSelect(sqlQuery.toString());
}
/**
* Retrieve IEntityLocks from the underlying store. Expiration must not be null.
*
* @param entityType Class
* @param entityKey String
* @param lockType Integer - so we can accept a null value.
* @param lockOwner String
* @exception LockingException - wraps an Exception specific to the store.
*/
private IEntityLock[] selectUnexpired(
Timestamp ts, Class entityType, String entityKey, Integer lockType, String lockOwner)
throws LockingException {
StringBuffer sqlQuery = new StringBuffer(getSelectSql());
sqlQuery.append(" WHERE " + EXPIRATION_TIME_COLUMN + GT + printTimestamp(ts));
if (entityType != null) {
Integer typeID = EntityTypesLocator.getEntityTypes().getEntityIDFromType(entityType);
sqlQuery.append(" AND " + ENTITY_TYPE_COLUMN + EQ + typeID);
}
if (entityKey != null) {
sqlQuery.append(" AND " + ENTITY_KEY_COLUMN + EQ + sqlQuote(entityKey));
}
if (lockType != null) {
sqlQuery.append(" AND " + LOCK_TYPE_COLUMN + EQ + lockType);
}
if (lockOwner != null) {
sqlQuery.append(" AND " + LOCK_OWNER_COLUMN + EQ + sqlQuote(lockOwner));
}
return primSelect(sqlQuery.toString());
}
/** @return org.apereo.portal.concurrency.locking.RDBMEntityLockStore */
public static synchronized IEntityLockStore singleton() throws LockingException {
if (singleton == null) {
singleton = new RDBMEntityLockStore();
}
return singleton;
}
/** @return java.lang.String */
private static java.lang.String sqlQuote(Object o) {
return QUOTE + o + QUOTE;
}
/**
* @param lock org.apereo.portal.groups.IEntityLock
* @param newExpiration java.util.Date
*/
public void update(IEntityLock lock, java.util.Date newExpiration) throws LockingException {
update(lock, newExpiration, null);
}
/**
* Updates the lock's <code>expiration</code> and <code>lockType</code> in the underlying store.
* Param <code>lockType</code> may be null.
*
* @param lock
* @param newExpiration java.util.Date
* @param newLockType Integer
*/
public void update(IEntityLock lock, Date newExpiration, Integer newLockType)
throws LockingException {
Connection conn = null;
try {
conn = RDBMServices.getConnection();
if (newLockType != null) {
primDeleteExpired(new Date(), lock.getEntityType(), lock.getEntityKey(), conn);
}
primUpdate(lock, newExpiration, newLockType, conn);
} catch (SQLException sqle) {
throw new LockingException("Problem updating " + lock, sqle);
} finally {
RDBMServices.releaseConnection(conn);
}
}
/** @return long */
private static long getTimestampMillis(Timestamp ts) {
if (timestampHasMillis) {
return ts.getTime();
} else {
return (ts.getTime() + ts.getNanos() / 1000000);
}
}
/** @return java.lang.String */
private static java.lang.String printTimestamp(Timestamp ts) {
return RDBMServices.getDbMetaData().sqlTimeStamp(getTimestampMillis(ts));
}
}