/* * Tigase Jabber/XMPP Server * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. Look for COPYING file in the top folder. * If not, see http://www.gnu.org/licenses/. * * $Rev$ * Last modified by $Author$ * $Date$ */ package tigase.server.amp; //~--- non-JDK imports -------------------------------------------------------- import java.security.NoSuchAlgorithmException; import java.sql.DataTruncation; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; import java.sql.Types; import java.util.Collections; import java.util.Date; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import tigase.db.DataRepository; import tigase.db.MsgRepositoryIfc; import tigase.db.RepositoryFactory; import tigase.db.UserNotFoundException; import tigase.server.Packet; import tigase.util.Algorithms; import tigase.util.SimpleCache; import tigase.xml.DomBuilderHandler; import tigase.xml.Element; import tigase.xml.SimpleParser; import tigase.xml.SingletonFactory; import tigase.xmpp.BareJID; import tigase.xmpp.JID; //~--- classes ---------------------------------------------------------------- /** * Created: May 3, 2010 5:28:02 PM * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class MsgRepository implements MsgRepositoryIfc { private static final Logger log = Logger.getLogger(MsgRepository.class.getName()); private static final String MSG_TABLE = "msg_history"; private static final String MSG_ID_COLUMN = "msg_id"; private static final String MSG_TIMESTAMP_COLUMN = "ts"; private static final String MSG_EXPIRED_COLUMN = "expired"; private static final String MSG_FROM_UID_COLUMN = "sender_uid"; private static final String MSG_TO_UID_COLUMN = "receiver_uid"; private static final String MSG_BODY_COLUMN = "message"; private static final String HISTORY_FLAG_COLUMN = "history_enabled"; private static final String JID_TABLE = "user_jid"; private static final String JID_ID_COLUMN = "jid_id"; private static final String JID_SHA_COLUMN = "jid_sha"; private static final String JID_COLUMN = "jid"; /* @formatter:off */ private static final String MYSQL_CREATE_MSG_TABLE = "create table " + MSG_TABLE + " ( " + " " + MSG_ID_COLUMN + " serial," + " " + MSG_TIMESTAMP_COLUMN + " TIMESTAMP DEFAULT CURRENT_TIMESTAMP," + " " + MSG_EXPIRED_COLUMN + " DATETIME," + " " + MSG_FROM_UID_COLUMN + " bigint unsigned," + " " + MSG_TO_UID_COLUMN + " bigint unsigned NOT NULL," + " " + MSG_BODY_COLUMN + " varchar(4096) NOT NULL," + " " + " key (" + MSG_EXPIRED_COLUMN + "), " + " key (" + MSG_FROM_UID_COLUMN + ", " + MSG_TO_UID_COLUMN + ")," + " key (" + MSG_TO_UID_COLUMN + ", " + MSG_FROM_UID_COLUMN + "))"; private static final String PGSQL_CREATE_MSG_TABLE = "create table " + MSG_TABLE + " ( " + " " + MSG_ID_COLUMN + " serial," + " " + MSG_TIMESTAMP_COLUMN + " TIMESTAMP DEFAULT CURRENT_TIMESTAMP," + " " + MSG_EXPIRED_COLUMN + " TIMESTAMP," + " " + MSG_FROM_UID_COLUMN + " bigint," + " " + MSG_TO_UID_COLUMN + " bigint NOT NULL," + " " + MSG_BODY_COLUMN + " varchar(4096) NOT NULL);" + "create index index_" + MSG_EXPIRED_COLUMN + " on " + MSG_TABLE + " (" + MSG_EXPIRED_COLUMN + ");" + "create index index_" + MSG_FROM_UID_COLUMN + "_" + MSG_TO_UID_COLUMN + " on " + MSG_TABLE + "("+MSG_FROM_UID_COLUMN + "," + MSG_TO_UID_COLUMN + ");" + "create index index_" + MSG_TO_UID_COLUMN + "_" + MSG_TO_UID_COLUMN + " on " + MSG_TABLE + "("+MSG_FROM_UID_COLUMN + "," + MSG_TO_UID_COLUMN + ");"; private static final String DERBY_CREATE_MSG_TABLE = "create table " + MSG_TABLE + " ( " + " " + MSG_ID_COLUMN + " bigint generated by default as identity not null," + " " + MSG_TIMESTAMP_COLUMN + " TIMESTAMP DEFAULT CURRENT_TIMESTAMP," + " " + MSG_EXPIRED_COLUMN + " TIMESTAMP," + " " + MSG_FROM_UID_COLUMN + " bigint," + " " + MSG_TO_UID_COLUMN + " bigint NOT NULL," + " " + MSG_BODY_COLUMN + " varchar(4096) NOT NULL);" + "create index index_" + MSG_EXPIRED_COLUMN + " on " + MSG_TABLE + " (" + MSG_EXPIRED_COLUMN + ");" + "create index index_" + MSG_FROM_UID_COLUMN + "_" + MSG_TO_UID_COLUMN + " on " + MSG_TABLE + "("+MSG_FROM_UID_COLUMN + "," + MSG_TO_UID_COLUMN + ");" + "create index index_" + MSG_TO_UID_COLUMN + "_" + MSG_TO_UID_COLUMN + " on " + MSG_TABLE + "("+MSG_FROM_UID_COLUMN + "," + MSG_TO_UID_COLUMN + ");"; private static final String MYSQL_CREATE_JID_TABLE = "create table " + JID_TABLE + " ( " + " " + JID_ID_COLUMN + " serial," + " " + JID_SHA_COLUMN + " char(128) NOT NULL," + " " + JID_COLUMN + " varchar(2049) NOT NULL," + " " + HISTORY_FLAG_COLUMN + " int default 0," + " primary key (" + JID_ID_COLUMN + ")," + " unique key " + JID_SHA_COLUMN + " (" + JID_SHA_COLUMN + ")," + " key " + JID_COLUMN + " (" + JID_COLUMN + "(765)))"; private static final String PGSQL_CREATE_JID_TABLE = "create table " + JID_TABLE + " ( " + " " + JID_ID_COLUMN + " serial," + " " + JID_SHA_COLUMN + " char(128) NOT NULL," + " " + JID_COLUMN + " varchar(2049) NOT NULL," + " " + HISTORY_FLAG_COLUMN + " int default 0," + " primary key (" + JID_ID_COLUMN + ")); " + "create unique index index_" + JID_SHA_COLUMN + " on " + JID_TABLE + " (" + JID_SHA_COLUMN + "); " + "create unique index index_" + JID_COLUMN + " on " + JID_TABLE + " (" + JID_COLUMN + "); "; private static final String DERBY_CREATE_JID_TABLE = "create table " + JID_TABLE + " ( " + " " + JID_ID_COLUMN + " bigint generated by default as identity not null," + " " + JID_SHA_COLUMN + " char(128) NOT NULL," + " " + JID_COLUMN + " varchar(2049) NOT NULL," + " " + HISTORY_FLAG_COLUMN + " int default 0," + " primary key (" + JID_ID_COLUMN + ")); " + "create unique index index_" + JID_SHA_COLUMN + " on " + JID_TABLE + " (" + JID_SHA_COLUMN + "); " + "create unique index index_" + JID_COLUMN + " on " + JID_TABLE + " (" + JID_COLUMN + "); "; private static final String MSG_INSERT_QUERY = "insert into " + MSG_TABLE + " ( " + MSG_EXPIRED_COLUMN + ", " + MSG_FROM_UID_COLUMN + ", " + MSG_TO_UID_COLUMN + ", " + MSG_BODY_COLUMN + ") values (?, ?, ?, ?)"; private static final String MSG_SELECT_TO_JID_QUERY = "select * from " + MSG_TABLE + " where " + MSG_TO_UID_COLUMN + " = ?"; private static final String MSG_DELETE_TO_JID_QUERY = "delete from " + MSG_TABLE + " where " + MSG_TO_UID_COLUMN + " = ?"; private static final String MSG_DELETE_ID_QUERY = "delete from " + MSG_TABLE + " where " + MSG_ID_COLUMN + " = ?"; private static final String MSG_SELECT_EXPIRED_QUERY = "select * from " + MSG_TABLE + " where expired is not null order by expired"; private static final String MSG_SELECT_EXPIRED_BEFORE_QUERY = "select * from " + MSG_TABLE + " where expired is not null and expired <= ? order by expired"; private static final String GET_USER_UID_DEF_QUERY = "select " + JID_ID_COLUMN + ", " + JID_COLUMN + " from " + JID_TABLE + " where " + JID_SHA_COLUMN + " = ?"; private static final String MSG_COUNT_FOR_TO_AND_FROM_QUERY_DEF = "select count(*) from " + MSG_TABLE + " where " + MSG_TO_UID_COLUMN + " = ? and " + MSG_FROM_UID_COLUMN + " = ?"; private static final String ADD_USER_JID_ID_QUERY = "insert into " + JID_TABLE + " ( " + JID_SHA_COLUMN + ", " + JID_COLUMN + ") values (?, ?)"; /* @formatter:on */ private static final String GET_USER_UID_PROP_KEY = "user-uid-query"; private static final String MSGS_STORE_LIMIT_KEY = "store-limit"; private static final String MSGS_COUNT_LIMIT_PROP_KEY = "count-limit-query"; private static final long MSGS_STORE_LIMIT_VAL = 100; private static final int MAX_UID_CACHE_SIZE = 100000; private static final long MAX_UID_CACHE_TIME = 3600000; private static final Map<String, MsgRepository> repos = new ConcurrentSkipListMap<String, MsgRepository>(); private static final int MAX_QUEUE_SIZE = 1000; // ~--- fields --------------------------------------------------------------- private DataRepository data_repo = null; private long earliestOffline = Long.MAX_VALUE; private SimpleParser parser = SingletonFactory.getParserInstance(); private String uid_query = GET_USER_UID_DEF_QUERY; private String msg_count_for_limit_query = MSG_COUNT_FOR_TO_AND_FROM_QUERY_DEF; private long msgs_store_limit = MSGS_STORE_LIMIT_VAL; private boolean initialized = false; private Map<BareJID, Long> uids_cache = Collections .synchronizedMap(new SimpleCache<BareJID, Long>(MAX_UID_CACHE_SIZE, MAX_UID_CACHE_TIME)); private DelayQueue<MsgDBItem> expiredQueue = new DelayQueue<MsgDBItem>(); // ~--- get methods ---------------------------------------------------------- /** * Method description * * @param id_string * @return */ public static MsgRepository getInstance(String id_string) { MsgRepository result = repos.get(id_string); if (result == null) { result = new MsgRepository(); repos.put(id_string, result); } return result; } /** * Method description * * @param time * @param delete * @return */ @Override public Element getMessageExpired(long time, boolean delete) { if (expiredQueue.size() == 0) { // If the queue is empty load it with some elements loadExpiredQueue(MAX_QUEUE_SIZE); } else { // If the queue is not empty, check whether recently saved off-line // message // is due to expire sooner then the head of the queue. MsgDBItem item = expiredQueue.peek(); if ((item != null) && (earliestOffline < item.expired.getTime())) { // There is in fact off-line message due to expire sooner then the head // of the // queue. Load all off-line message due to expire sooner then the first // element // in the queue. loadExpiredQueue(item.expired); } } MsgDBItem item = null; while (item == null) { try { item = expiredQueue.take(); } catch (InterruptedException ex) { } } if (delete) { deleteMessage(item.db_id); } return item.msg; } // ~--- methods -------------------------------------------------------------- /** * Method description * * @param conn_str * @param map * @throws SQLException */ public void initRepository(String conn_str, Map<String, String> map) throws SQLException { if (initialized) { return; } initialized = true; log.log(Level.INFO, "Initializing dbAccess for db connection url: {0}", conn_str); if (map != null) { String query = map.get(GET_USER_UID_PROP_KEY); if (query != null) { uid_query = query; } query = map.get(MsgRepository.MSGS_COUNT_LIMIT_PROP_KEY); if (query != null) { msg_count_for_limit_query = query; } String msgs_store_limit_str = map.get(MSGS_STORE_LIMIT_KEY); if (msgs_store_limit_str != null) { msgs_store_limit = Long.parseLong(msgs_store_limit_str); } } try { data_repo = RepositoryFactory.getDataRepository(null, conn_str, map); // Check if DB is correctly setup and contains all required tables. checkDB(); data_repo.initPreparedStatement(uid_query, uid_query); data_repo.initPreparedStatement(MSG_INSERT_QUERY, MSG_INSERT_QUERY); data_repo.initPreparedStatement(MSG_SELECT_TO_JID_QUERY, MSG_SELECT_TO_JID_QUERY); data_repo.initPreparedStatement(MSG_DELETE_TO_JID_QUERY, MSG_DELETE_TO_JID_QUERY); data_repo.initPreparedStatement(MSG_DELETE_ID_QUERY, MSG_DELETE_ID_QUERY); data_repo.initPreparedStatement(MSG_SELECT_EXPIRED_QUERY, MSG_SELECT_EXPIRED_QUERY); data_repo.initPreparedStatement(MSG_SELECT_EXPIRED_BEFORE_QUERY, MSG_SELECT_EXPIRED_BEFORE_QUERY); data_repo.initPreparedStatement(msg_count_for_limit_query, msg_count_for_limit_query); data_repo.initPreparedStatement(ADD_USER_JID_ID_QUERY, ADD_USER_JID_ID_QUERY); } catch (Exception e) { log.log(Level.WARNING, "MsgRepository not initialized due to exception", e); // Ignore for now.... } } /** * Method description * * @param to * @param delete * @return * @throws UserNotFoundException */ @Override public Queue<Element> loadMessagesToJID(JID to, boolean delete) throws UserNotFoundException { Queue<Element> result = null; ResultSet rs = null; try { long to_uid = getUserUID(to.getBareJID()); if (to_uid < 0) { throw new UserNotFoundException("User: " + to + " was not found in database."); } PreparedStatement select_to_jid_st = data_repo.getPreparedStatement(to.getBareJID(), MSG_SELECT_TO_JID_QUERY); synchronized (select_to_jid_st) { select_to_jid_st.setLong(1, to_uid); rs = select_to_jid_st.executeQuery(); StringBuilder sb = new StringBuilder(1000); while (rs.next()) { sb.append(rs.getString(MSG_BODY_COLUMN)); } if (sb.length() > 0) { DomBuilderHandler domHandler = new DomBuilderHandler(); parser.parse(domHandler, sb.toString().toCharArray(), 0, sb.length()); result = domHandler.getParsedElements(); } } if (delete) { PreparedStatement delete_to_jid_st = data_repo.getPreparedStatement(to.getBareJID(), MSG_DELETE_TO_JID_QUERY); synchronized (delete_to_jid_st) { delete_to_jid_st.setLong(1, to_uid); delete_to_jid_st.executeUpdate(); } } } catch (SQLException e) { log.log(Level.WARNING, "Problem getting offline messages for user: " + to, e); } finally { data_repo.release(null, rs); } return result; } /** * Method description * * @param from * @param to * @param expired * @param msg * @throws UserNotFoundException */ @Override public void storeMessage(JID from, JID to, Date expired, Element msg) throws UserNotFoundException { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Storring expired: {0} message: {1}", new Object[] { expired, Packet.elemToString(msg) }); } ResultSet rs = null; try { long from_uid = getUserUID(from.getBareJID()); if (from_uid < 0) { from_uid = addUserJID(from.getBareJID()); } long to_uid = getUserUID(to.getBareJID()); if (to_uid < 0) { to_uid = addUserJID(to.getBareJID()); } long count = 0; PreparedStatement count_msgs_st = data_repo.getPreparedStatement(to.getBareJID(), msg_count_for_limit_query); synchronized (count_msgs_st) { count_msgs_st.setLong(1, to_uid); count_msgs_st.setLong(2, from_uid); rs = count_msgs_st.executeQuery(); if (rs.next()) { count = rs.getLong(1); } } if (msgs_store_limit <= count) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Message store limit ({0}) exceeded for message: {1}", new Object[] { msgs_store_limit, Packet.elemToString(msg) }); } return; } PreparedStatement insert_msg_st = data_repo.getPreparedStatement(to.getBareJID(), MSG_INSERT_QUERY); synchronized (insert_msg_st) { if (expired == null) { insert_msg_st.setNull(1, Types.TIMESTAMP); } else { Timestamp time = new Timestamp(expired.getTime()); insert_msg_st.setTimestamp(1, time); } if (from_uid <= 0) { insert_msg_st.setNull(2, Types.BIGINT); } else { insert_msg_st.setLong(2, from_uid); } insert_msg_st.setLong(3, to_uid); // TODO: deal with messages bigger than the database can fit.... insert_msg_st.setString(4, msg.toString()); insert_msg_st.executeUpdate(); } if (expired != null) { if (expired.getTime() < earliestOffline) { earliestOffline = expired.getTime(); } if (expiredQueue.size() == 0) { loadExpiredQueue(1); } } } catch (DataTruncation dte) { log.log(Level.FINE, "Data truncated for message from {0} to {1}", new Object[] { from, to }); data_repo.release(null, rs); } catch (SQLException e) { log.log(Level.WARNING, "Problem adding new entry to DB: ", e); } } private long addUserJID(BareJID bareJID) throws SQLException, UserNotFoundException { try { String jid_sha = Algorithms.hexDigest(bareJID.toString(), "", "SHA"); PreparedStatement add_jid_id_st = data_repo.getPreparedStatement(bareJID, ADD_USER_JID_ID_QUERY); synchronized (add_jid_id_st) { add_jid_id_st.setString(1, jid_sha); add_jid_id_st.setString(2, bareJID.toString()); add_jid_id_st.executeUpdate(); } // // Give it some time or following select won't find a new entry MySQL // bug? // Thread.sleep(100); // }catch (InterruptedException ex) { // // // Do nothing } catch (NoSuchAlgorithmException ex) { log.log(Level.WARNING, "Configuration error or code bug: ", ex); return -1; } return getUserUID(bareJID); } private void checkDB() throws SQLException { if (data_repo.getResourceUri().contains("mysql")) { data_repo.checkTable(JID_TABLE, MYSQL_CREATE_JID_TABLE); data_repo.checkTable(MSG_TABLE, MYSQL_CREATE_MSG_TABLE); } else if (data_repo.getResourceUri().contains("postgresql")) { data_repo.checkTable(JID_TABLE, PGSQL_CREATE_JID_TABLE); data_repo.checkTable(MSG_TABLE, PGSQL_CREATE_MSG_TABLE); } else if (data_repo.getResourceUri().contains("derby")) { data_repo.checkTable(JID_TABLE, DERBY_CREATE_JID_TABLE); data_repo.checkTable(MSG_TABLE, DERBY_CREATE_MSG_TABLE); } } private void deleteMessage(long msg_id) { try { PreparedStatement delete_id_st = data_repo.getPreparedStatement(null, MSG_DELETE_ID_QUERY); synchronized (delete_id_st) { delete_id_st.setLong(1, msg_id); delete_id_st.executeUpdate(); } } catch (SQLException e) { log.log(Level.WARNING, "Problem removing entry from DB: ", e); } } // ~--- get methods ---------------------------------------------------------- private long getUserUID(BareJID user_id) throws SQLException, UserNotFoundException { Long cache_res = uids_cache.get(user_id); if (cache_res != null) { return cache_res.longValue(); } // end of if (result != null) ResultSet rs = null; long result = -1; String jid_sha; try { jid_sha = Algorithms.hexDigest(user_id.toString(), "", "SHA"); } catch (NoSuchAlgorithmException ex) { log.log(Level.WARNING, "Configuration error or code bug: ", ex); return -1; } try { PreparedStatement uid_st = data_repo.getPreparedStatement(user_id, uid_query); synchronized (uid_st) { uid_st.setString(1, jid_sha); rs = uid_st.executeQuery(); if (rs.next()) { BareJID res_jid = BareJID.bareJIDInstanceNS(rs.getString(JID_COLUMN)); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Found entry for JID: {0}, DB JID: {1}", new Object[] { user_id, res_jid }); } // There is a slight chance that there is the same SHA for 2 different // JIDs. // Even though it is impossible to store messages for both JIDs right // now // we have to make sure we don't send offline messages to incorrect // person if (user_id.equals(res_jid)) { result = rs.getLong(JID_ID_COLUMN); } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "JIDs don't match, SHA conflict? JID: {0}, DB JID: {1}", new Object[] { user_id, res_jid }); } } } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "No entry for JID: {0}", user_id); } } } // if (result <= 0) { // throw new UserNotFoundException("User does not exist: " + user_id); // } // end of if (isnext) else } finally { data_repo.release(null, rs); } if (result > 0) { uids_cache.put(user_id, result); } return result; } // ~--- methods -------------------------------------------------------------- private void loadExpiredQueue(int min_elements) { ResultSet rs = null; try { PreparedStatement select_expired_st = data_repo.getPreparedStatement(null, MSG_SELECT_EXPIRED_QUERY); synchronized (select_expired_st) { rs = select_expired_st.executeQuery(); DomBuilderHandler domHandler = new DomBuilderHandler(); int counter = 0; while (rs.next() && ((expiredQueue.size() < MAX_QUEUE_SIZE) || (counter++ < min_elements))) { String msg_str = rs.getString(MSG_BODY_COLUMN); parser.parse(domHandler, msg_str.toCharArray(), 0, msg_str.length()); Queue<Element> elems = domHandler.getParsedElements(); Element msg = elems.poll(); if (msg == null) { log.log(Level.INFO, "Something wrong, loaded offline message from DB but parsed no " + "XML elements: {0}", msg_str); } else { Timestamp ts = rs.getTimestamp(MSG_EXPIRED_COLUMN); MsgDBItem item = new MsgDBItem(rs.getLong(MSG_ID_COLUMN), msg, ts); expiredQueue.offer(item); } } } } catch (SQLException e) { log.log(Level.WARNING, "Problem getting offline messages from db: ", e); } finally { data_repo.release(null, rs); } earliestOffline = Long.MAX_VALUE; } private void loadExpiredQueue(Date expired) { ResultSet rs = null; try { if (expiredQueue.size() > 100 * MAX_QUEUE_SIZE) { expiredQueue.clear(); } PreparedStatement select_expired_before_st = data_repo.getPreparedStatement(null, MSG_SELECT_EXPIRED_BEFORE_QUERY); synchronized (select_expired_before_st) { select_expired_before_st.setTimestamp(1, new Timestamp(expired.getTime())); rs = select_expired_before_st.executeQuery(); DomBuilderHandler domHandler = new DomBuilderHandler(); int counter = 0; while (rs.next() && (counter++ < MAX_QUEUE_SIZE)) { String msg_str = rs.getString(MSG_BODY_COLUMN); parser.parse(domHandler, msg_str.toCharArray(), 0, msg_str.length()); Queue<Element> elems = domHandler.getParsedElements(); Element msg = elems.poll(); if (msg == null) { log.log(Level.INFO, "Something wrong, loaded offline message from DB but parsed no " + "XML elements: {0}", msg_str); } else { Timestamp ts = rs.getTimestamp(MSG_EXPIRED_COLUMN); MsgDBItem item = new MsgDBItem(rs.getLong(MSG_ID_COLUMN), msg, ts); expiredQueue.offer(item); } } } } catch (SQLException e) { log.log(Level.WARNING, "Problem getting offline messages from db: ", e); } finally { data_repo.release(null, rs); } earliestOffline = Long.MAX_VALUE; } // ~--- inner classes -------------------------------------------------------- private class MsgDBItem implements Delayed { private long db_id = -1; private Date expired = null; private Element msg = null; // ~--- constructors ------------------------------------------------------- /** * Constructs ... * * @param db_id * @param msg * @param expired */ public MsgDBItem(long db_id, Element msg, Date expired) { this.db_id = db_id; this.msg = msg; this.expired = expired; } // ~--- methods ------------------------------------------------------------ /** * Method description * * @param o * @return */ @Override public int compareTo(Delayed o) { return (int) (getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS)); } // ~--- get methods -------------------------------------------------------- /** * Method description * * @param unit * @return */ @Override public long getDelay(TimeUnit unit) { return unit.convert(expired.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } } } // ~ Formatted in Sun Code Convention // ~ Formatted by Jindent --- http://www.jindent.com