001: /* Copyright 2002 The JA-SIG Collaborative. All rights reserved.
002: * See license distributed with this file and
003: * available online at http://www.uportal.org/license.html
004: */
005:
006: package org.jasig.portal.concurrency.locking;
007:
008: import java.sql.Connection;
009: import java.sql.PreparedStatement;
010: import java.sql.ResultSet;
011: import java.sql.SQLException;
012: import java.sql.Statement;
013: import java.sql.Timestamp;
014: import java.util.ArrayList;
015: import java.util.Date;
016: import java.util.List;
017:
018: import org.apache.commons.logging.Log;
019: import org.apache.commons.logging.LogFactory;
020: import org.jasig.portal.EntityTypes;
021: import org.jasig.portal.RDBMServices;
022: import org.jasig.portal.concurrency.IEntityLock;
023: import org.jasig.portal.concurrency.LockingException;
024:
025: /**
026: * RDBMS-based store for <code>IEntityLocks</code>.
027: * @author Dan Ellentuck
028: * @version $Revision: 35555 $
029: */
030: public class RDBMEntityLockStore implements IEntityLockStore {
031: private static final Log log = LogFactory
032: .getLog(RDBMEntityLockStore.class);
033: private static IEntityLockStore singleton;
034:
035: // Constants for the LOCK table:
036: private static String LOCK_TABLE = "UP_ENTITY_LOCK";
037: private static String ENTITY_TYPE_COLUMN = "ENTITY_TYPE_ID";
038: private static String ENTITY_KEY_COLUMN = "ENTITY_KEY";
039: private static String EXPIRATION_TIME_COLUMN = "EXPIRATION_TIME";
040: private static String LOCK_OWNER_COLUMN = "LOCK_OWNER";
041: private static String LOCK_TYPE_COLUMN = "LOCK_TYPE";
042: private static String EQ = " = ";
043: private static String GT = " > ";
044: private static String LT = " < ";
045: private static String QUOTE = "'";
046:
047: private static String allLockColumns;
048: private static String addSql;
049: private static String deleteLockSql;
050: private static String updateSql;
051:
052: // Prior to jdk 1.4, java.sql.Timestamp.getTime() truncated milliseconds.
053: private static boolean timestampHasMillis;
054: static {
055: Date testDate = new Date();
056: Timestamp testTimestamp = new Timestamp(testDate.getTime());
057: timestampHasMillis = (testDate.getTime() == testTimestamp
058: .getTime());
059: }
060:
061: /**
062: * RDBMEntityGroupStore constructor.
063: */
064: public RDBMEntityLockStore() throws LockingException {
065: super ();
066: initialize();
067: }
068:
069: /**
070: * Adds the lock to the underlying store.
071: * @param lock
072: */
073: public void add(IEntityLock lock) throws LockingException {
074: Connection conn = null;
075: try {
076: conn = RDBMServices.getConnection();
077: primDeleteExpired(new Date(), lock.getEntityType(), lock
078: .getEntityKey(), conn);
079: primAdd(lock, conn);
080: }
081:
082: catch (SQLException sqle) {
083: throw new LockingException("Problem creating " + lock, sqle);
084: }
085:
086: finally {
087: RDBMServices.releaseConnection(conn);
088: }
089: }
090:
091: /**
092: * If this IEntityLock exists, delete it.
093: * @param lock
094: */
095: public void delete(IEntityLock lock) throws LockingException {
096: Connection conn = null;
097: try {
098: conn = RDBMServices.getConnection();
099: primDelete(lock, conn);
100: }
101:
102: catch (SQLException sqle) {
103: throw new LockingException("Problem deleting " + lock, sqle);
104: } finally {
105: RDBMServices.releaseConnection(conn);
106: }
107: }
108:
109: /**
110: * Delete all IEntityLocks from the underlying store.
111: */
112: public void deleteAll() throws LockingException {
113: Connection conn = null;
114: Statement stmnt = null;
115: try {
116: String sql = "DELETE FROM " + LOCK_TABLE;
117: if (log.isDebugEnabled())
118: log.debug("RDBMEntityLockStore.deleteAll(): " + sql);
119:
120: conn = RDBMServices.getConnection();
121: try {
122: stmnt = conn.createStatement();
123: int rc = stmnt.executeUpdate(sql);
124: if (log.isDebugEnabled()) {
125: String msg = "Deleted " + rc + " locks.";
126: log
127: .debug("RDBMEntityLockStore.deleteAll(): "
128: + msg);
129: }
130: } finally {
131: if (stmnt != null)
132: stmnt.close();
133: }
134: } catch (SQLException sqle) {
135: throw new LockingException("Problem deleting locks", sqle);
136: }
137:
138: finally {
139: RDBMServices.releaseConnection(conn);
140: }
141: }
142:
143: /**
144: * Delete all expired IEntityLocks from the underlying store.
145: * @param expiration
146: */
147: public void deleteExpired(Date expiration) throws LockingException {
148: deleteExpired(expiration, null, null);
149: }
150:
151: /**
152: * Delete IEntityLocks from the underlying store that have expired as of
153: * <code>expiration</code>. Params <code>entityType</code> and
154: * <code>entityKey</code> are optional.
155: *
156: * @param expiration java.util.Date
157: * @param entityType Class
158: * @param entityKey String
159: */
160: public void deleteExpired(Date expiration, Class entityType,
161: String entityKey) throws LockingException {
162: Connection conn = null;
163: try {
164: conn = RDBMServices.getConnection();
165: primDeleteExpired(expiration, entityType, entityKey, conn);
166: }
167:
168: catch (SQLException sqle) {
169: throw new LockingException(
170: "Problem deleting expired locks", sqle);
171: } finally {
172: RDBMServices.releaseConnection(conn);
173: }
174: }
175:
176: /**
177: * Delete all expired IEntityLocks from the underlying store.
178: * @param lock IEntityLock
179: */
180: public void deleteExpired(IEntityLock lock) throws LockingException {
181: deleteExpired(new Date(), lock.getEntityType(), lock
182: .getEntityKey());
183: }
184:
185: /**
186: * Retrieve IEntityLocks from the underlying store. Any or all of the parameters
187: * may be null.
188: * @param entityType Class
189: * @param entityKey String
190: * @param lockType Integer - so we can accept a null value.
191: * @param expiration Date
192: * @param lockOwner String
193: * @exception LockingException - wraps an Exception specific to the store.
194: */
195: public IEntityLock[] find(Class entityType, String entityKey,
196: Integer lockType, Date expiration, String lockOwner)
197: throws LockingException {
198: return select(entityType, entityKey, lockType, expiration,
199: lockOwner);
200: }
201:
202: /**
203: * Retrieve IEntityLocks from the underlying store. Expiration must not be null.
204: * @param expiration Date
205: * @param entityType Class
206: * @param entityKey String
207: * @param lockType Integer - so we can accept a null value.
208: * @param lockOwner String
209: * @exception LockingException - wraps an Exception specific to the store.
210: */
211: public IEntityLock[] findUnexpired(Date expiration,
212: Class entityType, String entityKey, Integer lockType,
213: String lockOwner) throws LockingException {
214: Timestamp ts = new Timestamp(expiration.getTime());
215: return selectUnexpired(ts, entityType, entityKey, lockType,
216: lockOwner);
217: }
218:
219: /**
220: * SQL for inserting a row into the lock table.
221: */
222: private static String getAddSql() {
223: if (addSql == null) {
224: addSql = "INSERT INTO " + LOCK_TABLE + "("
225: + getAllLockColumns() + ") VALUES (?, ?, ?, ?, ?)";
226: }
227: return addSql;
228: }
229:
230: /**
231: * @return java.lang.String
232: */
233: private static java.lang.String getAllLockColumns() {
234: if (allLockColumns == null) {
235: StringBuffer buff = new StringBuffer(100);
236: buff.append(ENTITY_TYPE_COLUMN);
237: buff.append(", ");
238: buff.append(ENTITY_KEY_COLUMN);
239: buff.append(", ");
240: buff.append(LOCK_TYPE_COLUMN);
241: buff.append(", ");
242: buff.append(EXPIRATION_TIME_COLUMN);
243: buff.append(", ");
244: buff.append(LOCK_OWNER_COLUMN);
245:
246: allLockColumns = buff.toString();
247: }
248: return allLockColumns;
249: }
250:
251: /**
252: * SQL for deleting a row on the lock table.
253: */
254: private static String getDeleteLockSql() {
255: if (deleteLockSql == null) {
256: deleteLockSql = "DELETE FROM " + LOCK_TABLE + " WHERE "
257: + ENTITY_TYPE_COLUMN + EQ + "?" + " AND "
258: + ENTITY_KEY_COLUMN + EQ + "?" + " AND "
259: + EXPIRATION_TIME_COLUMN + EQ + "?" + " AND "
260: + LOCK_TYPE_COLUMN + EQ + "?" + " AND "
261: + LOCK_OWNER_COLUMN + EQ + "?";
262: }
263: return deleteLockSql;
264: }
265:
266: /**
267: * @return java.lang.String
268: */
269: private static java.lang.String getSelectSql() {
270: return ("SELECT " + getAllLockColumns() + " FROM " + LOCK_TABLE);
271: }
272:
273: /**
274: * SQL for updating a row on the lock table.
275: */
276: private static String getUpdateSql() {
277: if (updateSql == null) {
278: updateSql = "UPDATE " + LOCK_TABLE + " SET "
279: + EXPIRATION_TIME_COLUMN + EQ + "?, "
280: + LOCK_TYPE_COLUMN + EQ + "?" + " WHERE "
281: + ENTITY_TYPE_COLUMN + EQ + "?" + " AND "
282: + ENTITY_KEY_COLUMN + EQ + "?" + " AND "
283: + LOCK_OWNER_COLUMN + EQ + "?" + " AND "
284: + EXPIRATION_TIME_COLUMN + EQ + "?" + " AND "
285: + LOCK_TYPE_COLUMN + EQ + "?";
286: }
287: return updateSql;
288: }
289:
290: /**
291: * Cleanup the store by deleting locks expired an hour ago.
292: */
293: private void initialize() throws LockingException {
294: Date expiration = new Date(System.currentTimeMillis()
295: - (60 * 60 * 1000));
296: deleteExpired(expiration, null, null);
297: }
298:
299: /**
300: * Extract values from ResultSet and create a new lock.
301: * @return org.jasig.portal.groups.IEntityLock
302: * @param rs java.sql.ResultSet
303: */
304: private IEntityLock instanceFromResultSet(java.sql.ResultSet rs)
305: throws SQLException, LockingException {
306: Integer entityTypeID = new Integer(rs.getInt(1));
307: Class entityType = EntityTypes.getEntityType(entityTypeID);
308: String key = rs.getString(2);
309: int lockType = rs.getInt(3);
310: Timestamp ts = rs.getTimestamp(4);
311: String lockOwner = rs.getString(5);
312:
313: return newInstance(entityType, key, lockType, ts, lockOwner);
314: }
315:
316: /**
317: * @return org.jasig.portal.concurrency.locking.IEntityLock
318: */
319: private IEntityLock newInstance(Class entityType, String entityKey,
320: int lockType, Date expirationTime, String lockOwner)
321: throws LockingException {
322: return new EntityLockImpl(entityType, entityKey, lockType,
323: expirationTime, lockOwner);
324: }
325:
326: /**
327: * Add the lock to the underlying store.
328: * @param lock org.jasig.portal.concurrency.locking.IEntityLock
329: * @param conn java.sql.Connection
330: */
331: private void primAdd(IEntityLock lock, Connection conn)
332: throws SQLException, LockingException {
333: Integer typeID = EntityTypes.getEntityTypeID(lock
334: .getEntityType());
335: String key = lock.getEntityKey();
336: int lockType = lock.getLockType();
337: Timestamp ts = new Timestamp(lock.getExpirationTime().getTime());
338: String owner = lock.getLockOwner();
339:
340: try {
341: PreparedStatement ps = conn.prepareStatement(getAddSql());
342: try {
343: ps.setInt(1, typeID.intValue()); // entity type
344: ps.setString(2, key); // entity key
345: ps.setInt(3, lockType); // lock type
346: ps.setTimestamp(4, ts); // lock expiration
347: ps.setString(5, owner); // lock owner
348:
349: if (log.isDebugEnabled())
350: log.debug("RDBMEntityLockStore.primAdd(): " + ps);
351:
352: int rc = ps.executeUpdate();
353: if (rc != 1) {
354: String errString = "Problem adding " + lock;
355: log.error(errString);
356: throw new LockingException(errString);
357: }
358: } finally {
359: if (ps != null)
360: ps.close();
361: }
362: } catch (java.sql.SQLException sqle) {
363: log.error(sqle, sqle);
364: throw sqle;
365: }
366: }
367:
368: /**
369: * Delete the IEntityLock from the underlying store.
370: * @param lock
371: * @param conn the database connection
372: */
373: private void primDelete(IEntityLock lock, Connection conn)
374: throws LockingException, SQLException {
375: Integer typeID = EntityTypes.getEntityTypeID(lock
376: .getEntityType());
377: String key = lock.getEntityKey();
378: int lockType = lock.getLockType();
379: Timestamp ts = new Timestamp(lock.getExpirationTime().getTime());
380: String owner = lock.getLockOwner();
381:
382: try {
383: PreparedStatement ps = conn
384: .prepareStatement(getDeleteLockSql());
385: try {
386: ps.setInt(1, typeID.intValue()); // entity type
387: ps.setString(2, key); // entity key
388: ps.setTimestamp(3, ts); // lock expiration
389: ps.setInt(4, lockType); // lock type
390: ps.setString(5, owner); // lock owner
391:
392: if (log.isDebugEnabled())
393: log
394: .debug("RDBMEntityLockStore.primDelete(): "
395: + ps);
396:
397: int rc = ps.executeUpdate();
398: if (log.isDebugEnabled())
399: log
400: .debug("RDBMEntityLockStore.primDelete(): deleted "
401: + rc + " lock(s).");
402: } finally {
403: if (ps != null)
404: ps.close();
405: }
406: } catch (java.sql.SQLException sqle) {
407: log.error(sqle, sqle);
408: throw sqle;
409: }
410: }
411:
412: /**
413: * Delete IEntityLocks from the underlying store that have expired as of
414: * <code>expiration</code>. Params <code>entityType</code> and
415: * <code>entityKey</code> are optional.
416: *
417: * @param expiration java.util.Date
418: * @param entityType Class
419: * @param entityKey String
420: * @param conn Connection
421: */
422: private void primDeleteExpired(Date expiration, Class entityType,
423: String entityKey, Connection conn) throws LockingException,
424: SQLException {
425: Statement stmnt = null;
426: Timestamp ts = new Timestamp(expiration.getTime());
427:
428: StringBuffer buff = new StringBuffer(100);
429: buff.append("DELETE FROM " + LOCK_TABLE + " WHERE "
430: + EXPIRATION_TIME_COLUMN + LT);
431: buff.append(printTimestamp(ts));
432: if (entityType != null) {
433: Integer typeID = EntityTypes.getEntityTypeID(entityType);
434: buff.append(" AND " + ENTITY_TYPE_COLUMN + EQ + typeID);
435: }
436: if (entityKey != null) {
437: buff.append(" AND " + ENTITY_KEY_COLUMN + EQ
438: + sqlQuote(entityKey));
439: }
440:
441: String sql = buff.toString();
442:
443: if (log.isDebugEnabled())
444: log.debug("RDBMEntityLockStore.deleteExpired(): " + sql);
445:
446: try {
447: stmnt = conn.createStatement();
448: int rc = stmnt.executeUpdate(sql);
449: if (log.isDebugEnabled()) {
450: String msg = "Deleted " + rc + " expired locks.";
451: log
452: .debug("RDBMEntityLockStore.deleteExpired(): "
453: + msg);
454: }
455:
456: }
457:
458: catch (SQLException sqle) {
459: throw new LockingException(
460: "Problem deleting expired locks", sqle);
461: }
462:
463: finally {
464: if (stmnt != null)
465: stmnt.close();
466: }
467: }
468:
469: /**
470: * Retrieve IEntityLocks from the underlying store.
471: * @param sql String - the sql string used to select the entity lock rows.
472: * @exception LockingException - wraps an Exception specific to the store.
473: */
474: private IEntityLock[] primSelect(String sql)
475: throws LockingException {
476: Connection conn = null;
477: Statement stmnt = null;
478: ResultSet rs = null;
479: List locks = new ArrayList();
480:
481: if (log.isDebugEnabled())
482: log.debug("RDBMEntityLockStore.primSelect(): " + sql);
483:
484: try {
485: conn = RDBMServices.getConnection();
486: stmnt = conn.createStatement();
487: try {
488: rs = stmnt.executeQuery(sql);
489: try {
490: while (rs.next()) {
491: locks.add(instanceFromResultSet(rs));
492: }
493: } finally {
494: rs.close();
495: }
496: } finally {
497: stmnt.close();
498: }
499: } catch (SQLException sqle) {
500: log.error(sqle, sqle);
501: throw new LockingException(
502: "Problem retrieving EntityLocks", sqle);
503: } finally {
504: RDBMServices.releaseConnection(conn);
505: }
506:
507: return ((IEntityLock[]) locks.toArray(new IEntityLock[locks
508: .size()]));
509: }
510:
511: /**
512: * Updates the lock's <code>expiration</code> and <code>lockType</code> in the
513: * underlying store. The SQL is over-qualified to make sure the row has not been
514: * updated since the lock was last checked.
515: * @param lock
516: * @param newExpiration java.util.Date
517: * @param newType Integer
518: * @param conn Connection
519: */
520: private void primUpdate(IEntityLock lock, Date newExpiration,
521: Integer newType, Connection conn) throws SQLException,
522: LockingException {
523: Integer typeID = EntityTypes.getEntityTypeID(lock
524: .getEntityType());
525: String key = lock.getEntityKey();
526: int oldLockType = lock.getLockType();
527: int newLockType = (newType == null) ? oldLockType : newType
528: .intValue();
529: java.sql.Timestamp oldTs = new java.sql.Timestamp(lock
530: .getExpirationTime().getTime());
531: java.sql.Timestamp newTs = new java.sql.Timestamp(newExpiration
532: .getTime());
533: String owner = lock.getLockOwner();
534:
535: try {
536: PreparedStatement ps = conn
537: .prepareStatement(getUpdateSql());
538: try {
539: ps.setTimestamp(1, newTs); // new expiration
540: ps.setInt(2, newLockType); // new lock type
541: ps.setInt(3, typeID.intValue()); // entity type
542: ps.setString(4, key); // entity key
543: ps.setString(5, owner); // lock owner
544: ps.setTimestamp(6, oldTs); // old expiration
545: ps.setInt(7, oldLockType); // old lock type;
546:
547: if (log.isDebugEnabled())
548: log
549: .debug("RDBMEntityLockStore.primUpdate(): "
550: + ps);
551:
552: int rc = ps.executeUpdate();
553: if (rc != 1) {
554: String errString = "Problem updating " + lock;
555: log.error(errString);
556: throw new LockingException(errString);
557: }
558: } finally {
559: if (ps != null)
560: ps.close();
561: }
562: } catch (java.sql.SQLException sqle) {
563: log.error(sqle, sqle);
564: throw sqle;
565: }
566: }
567:
568: /**
569: * Retrieve IEntityLocks from the underlying store. Any or all of the parameters
570: * may be null.
571: * @param entityType Class
572: * @param entityKey String
573: * @param lockType Integer - so we can accept a null value.
574: * @param expiration Date
575: * @param lockOwner String
576: * @exception LockingException - wraps an Exception specific to the store.
577: */
578: private IEntityLock[] select(Class entityType, String entityKey,
579: Integer lockType, Date expiration, String lockOwner)
580: throws LockingException {
581: StringBuffer sqlQuery = new StringBuffer(getSelectSql()
582: + " WHERE 1 = 1");
583:
584: if (entityType != null) {
585: Integer typeID = EntityTypes.getEntityTypeID(entityType);
586: sqlQuery.append(" AND " + ENTITY_TYPE_COLUMN + EQ + typeID);
587: }
588:
589: if (entityKey != null) {
590: sqlQuery.append(" AND " + ENTITY_KEY_COLUMN + EQ
591: + sqlQuote(entityKey));
592: }
593:
594: if (lockType != null) {
595: sqlQuery.append(" AND " + LOCK_TYPE_COLUMN + EQ + lockType);
596: }
597:
598: if (expiration != null) {
599: Timestamp ts = new Timestamp(expiration.getTime());
600: sqlQuery.append(" AND " + EXPIRATION_TIME_COLUMN + EQ
601: + printTimestamp(ts));
602: }
603:
604: if (lockOwner != null) {
605: sqlQuery.append(" AND " + LOCK_OWNER_COLUMN + EQ
606: + sqlQuote(lockOwner));
607: }
608:
609: return primSelect(sqlQuery.toString());
610: }
611:
612: /**
613: * Retrieve IEntityLocks from the underlying store. Expiration must not be null.
614: * @param entityType Class
615: * @param entityKey String
616: * @param lockType Integer - so we can accept a null value.
617: * @param lockOwner String
618: * @exception LockingException - wraps an Exception specific to the store.
619: */
620: private IEntityLock[] selectUnexpired(Timestamp ts,
621: Class entityType, String entityKey, Integer lockType,
622: String lockOwner) throws LockingException {
623: StringBuffer sqlQuery = new StringBuffer(getSelectSql());
624:
625: sqlQuery.append(" WHERE " + EXPIRATION_TIME_COLUMN + GT
626: + printTimestamp(ts));
627:
628: if (entityType != null) {
629: Integer typeID = EntityTypes.getEntityTypeID(entityType);
630: sqlQuery.append(" AND " + ENTITY_TYPE_COLUMN + EQ + typeID);
631: }
632:
633: if (entityKey != null) {
634: sqlQuery.append(" AND " + ENTITY_KEY_COLUMN + EQ
635: + sqlQuote(entityKey));
636: }
637:
638: if (lockType != null) {
639: sqlQuery.append(" AND " + LOCK_TYPE_COLUMN + EQ + lockType);
640: }
641:
642: if (lockOwner != null) {
643: sqlQuery.append(" AND " + LOCK_OWNER_COLUMN + EQ
644: + sqlQuote(lockOwner));
645: }
646:
647: return primSelect(sqlQuery.toString());
648: }
649:
650: /**
651: * @return org.jasig.portal.concurrency.locking.RDBMEntityLockStore
652: */
653: public static synchronized IEntityLockStore singleton()
654: throws LockingException {
655: if (singleton == null) {
656: singleton = new RDBMEntityLockStore();
657: }
658: return singleton;
659: }
660:
661: /**
662: * @return java.lang.String
663: */
664: private static java.lang.String sqlQuote(Object o) {
665: return QUOTE + o + QUOTE;
666: }
667:
668: /**
669: * @param lock org.jasig.portal.groups.IEntityLock
670: * @param newExpiration java.util.Date
671: */
672: public void update(IEntityLock lock, java.util.Date newExpiration)
673: throws LockingException {
674: update(lock, newExpiration, null);
675: }
676:
677: /**
678: * Updates the lock's <code>expiration</code> and <code>lockType</code> in the
679: * underlying store. Param <code>lockType</code> may be null.
680: * @param lock
681: * @param newExpiration java.util.Date
682: * @param newLockType Integer
683: */
684: public void update(IEntityLock lock, Date newExpiration,
685: Integer newLockType) throws LockingException {
686: Connection conn = null;
687: try {
688: conn = RDBMServices.getConnection();
689: if (newLockType != null) {
690: primDeleteExpired(new Date(), lock.getEntityType(),
691: lock.getEntityKey(), conn);
692: }
693: primUpdate(lock, newExpiration, newLockType, conn);
694: }
695:
696: catch (SQLException sqle) {
697: throw new LockingException("Problem updating " + lock, sqle);
698: } finally {
699: RDBMServices.releaseConnection(conn);
700: }
701: }
702:
703: /**
704: * @return long
705: */
706: private static long getTimestampMillis(Timestamp ts) {
707: if (timestampHasMillis) {
708: return ts.getTime();
709: } else {
710: return (ts.getTime() + ts.getNanos() / 1000000);
711: }
712: }
713:
714: /**
715: * @return java.lang.String
716: */
717: private static java.lang.String printTimestamp(Timestamp ts) {
718: return RDBMServices.getDbMetaData().sqlTimeStamp(
719: getTimestampMillis(ts));
720: }
721: }
|