// // $Id$ package com.threerings.user; import static com.threerings.user.Log.log; import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.List; import com.samskivert.io.PersistenceException; import com.samskivert.jdbc.ConnectionProvider; import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.jdbc.JDBCUtil; import com.samskivert.jdbc.JORARepository; import com.samskivert.jdbc.jora.Cursor; import com.samskivert.jdbc.jora.FieldMask; import com.samskivert.jdbc.jora.Table; /** * Maintains persistent reward information. */ public class RewardRepository extends JORARepository { /** * Creates the repository with the specified connection provider. * * @exception PersistenceException thrown if an error occurs communicating with the underlying * persistence facilities. */ public RewardRepository (ConnectionProvider provider) throws PersistenceException { super(provider, OOOUserRepository.USER_REPOSITORY_IDENT); } /** * Creates a new RewardInfo record in the database. info should have the * description, data and expiration filled in. */ public void createReward (RewardInfo info) throws PersistenceException { info.maxEligibleId = getMaxUserId(); store(_infoTable, info); } /** * Immediately expires a reward by setting the expiration to the current date then making a * call to purgeExpiredRewards. */ public void expireReward (int rewardId) throws PersistenceException { RewardInfo info = new RewardInfo(); info.rewardId = rewardId; info.expiration = new Date(System.currentTimeMillis()); updateField(_infoTable, info, "expiration"); purgeExpiredRewards(); } /** * Attempt to activate the reward for an account. If this account has not already activated * the reward, a new RewardRecord will be created. Returns true if a new RewardRecord is * created, false otherwise. */ public boolean activateReward (int rewardId, String account) throws PersistenceException { return activateReward(rewardId, account, null); } public boolean activateReward (int rewardId, String account, String param) throws PersistenceException { final RewardRecord rr = new RewardRecord(); rr.rewardId = rewardId; rr.account = account; rr.param = param == null ? "" : param; // try to insert it; catching duplicate row exceptions and reporting that the reward is // already activated return executeUpdate(new Operation() { public Boolean invoke (Connection conn, DatabaseLiaison liaison) throws SQLException, PersistenceException { try { _rewardsTable.insert(conn, rr); return true; } catch (SQLException sqe) { if (liaison.isDuplicateRowException(sqe)) { return false; } else { throw sqe; } } } }); } /** * Activates monthly rewards of the specified ID for the account between start and end. If they * already have monthly rewards for this ID, will add new ones only at appropriate monthly * intervals from the existing ones. */ public boolean activateMonthlyRewards (final int rewardId, final String account, final java.util.Date start, final java.util.Date end) throws PersistenceException { // Setup an example to find relevant rewards. final RewardRecord example = new RewardRecord(); example.rewardId = rewardId; example.account = account; final FieldMask mask = _rewardsTable.getFieldMask(); mask.setModified("rewardId"); mask.setModified("account"); final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return executeUpdate(new Operation() { public Boolean invoke (Connection conn, DatabaseLiaison liaison) throws SQLException, PersistenceException { // Find the most recent reward this account already has of this type. java.util.Date latest = null; Cursor matches = _rewardsTable.queryByExample(conn, example, mask); RewardRecord rec; while ((rec = matches.next()) != null) { if (rec.param == null) { log.warning("Missing monthly reward param: ", "rewardId", rewardId, "account", account); continue; } try { java.util.Date thisDate = dateFormat.parse(rec.param); if (latest == null || thisDate.after(latest)) { latest = thisDate; } } catch (ParseException pe) { log.warning("Bogus reward param: ", "rewardId", rewardId, "account", account, "param", rec.param); continue; } } // We use the maximum of our provided start time and a month-after-latest // as the first time we'll input a reward. Note that this can result // in no rewards being given. Calendar startCal = Calendar.getInstance(); if (latest != null) { startCal.setTime(latest); startCal.add(Calendar.MONTH, 1); if (startCal.getTime().before(start)) { startCal.setTime(start); } } else { startCal.setTime(start); } // Add in any appropriate rewards between the modified start and end. while (startCal.getTime().before(end)) { final RewardRecord rr = new RewardRecord(); rr.rewardId = rewardId; rr.account = account; rr.param = dateFormat.format(startCal.getTime()); _rewardsTable.insert(conn, rr); startCal.add(Calendar.MONTH, 1); } return true; } }); } /** * Deactivates all rewards of the specified ID and account during the time window. * Returns the number of rewards deleted. */ public int deactivateMonthlyRewards (final int rewardId, final String account, final java.util.Date start, final java.util.Date end) throws PersistenceException { return executeUpdate(new Operation() { public Integer invoke (Connection conn, DatabaseLiaison liaison) throws SQLException, PersistenceException { String delQuery = "delete from REWARD_RECORDS where REWARD_ID = ? and ACCOUNT = ?" + " and PARAM >= ? and PARAM <= ? and REDEEMER_IDENT is null"; final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String startStr = dateFormat.format(start); String endStr = dateFormat.format(end); PreparedStatement stmt = null; try { stmt = conn.prepareStatement(delQuery); stmt.setInt(1, rewardId); stmt.setString(2, account); stmt.setString(3, startStr); stmt.setString(4, endStr); return stmt.executeUpdate(); } finally { JDBCUtil.close(stmt); } } }); } /** * Returns all currently active rewards. */ public List loadActiveRewards () throws PersistenceException { return loadAll(_infoTable, "where EXPIRATION > NOW()"); } /** * Returns all rewards. */ public List loadRewards () throws PersistenceException { return loadAll(_infoTable, "order by REWARD_ID desc"); } /** * Returns all RewardRecords that match the given account name and whose eligible * date is in the past. The returned list will be sorted by rewardId. */ public List loadActivatedRewards (String account) throws PersistenceException { String condition = "where ACCOUNT = " + JDBCUtil.escape(account) + " order by REWARD_ID"; return loadAll(_rewardsTable, condition); } /** * Returns all RewardRecords that match either the account or redeemer identifier. * The returned list will be sorted by rewardId. */ public List loadActivatedRewards (String account, String redeemerIdent) throws PersistenceException { String condition = "where ACCOUNT = " + JDBCUtil.escape(account) + " or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + " order by REWARD_ID, PARAM"; return loadAll(_rewardsTable, condition); } /** * Returns all RewardRecords for a specified rewardId that match * either the account or redeemer identifier. */ public List loadActivatedReward ( String account, String redeemerIdent, int rewardId) throws PersistenceException { String condition = "where REWARD_ID = " + rewardId + " and (ACCOUNT = " + JDBCUtil.escape(account) + " or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + ")"; return loadAll(_rewardsTable, condition); } /** * Returns all RewardRecords for a specified rewardId and * param that match either the account or redeemer identifier. */ public List loadActivatedReward ( String account, String redeemerIdent, int rewardId, String param) throws PersistenceException { String paramStr = (param == null) ? "PARAM is NULL" : "PARAM = " + JDBCUtil.escape(param); String condition = "where REWARD_ID = " + rewardId + " and " + paramStr + " and (ACCOUNT = " + JDBCUtil.escape(account) + " or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + ")"; return loadAll(_rewardsTable, condition); } /** * Updates the supplied RewardRecord with the indicated redeemerIdent and store it * to the database. */ public boolean redeemReward (RewardRecord record, String redeemerIdent) throws PersistenceException { String update = "update " + _rewardsTable.getName() + " set REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + " where REDEEMER_IDENT is NULL and REWARD_ID = " + record.rewardId + " and ACCOUNT = " + JDBCUtil.escape(record.account) + " and PARAM = " + JDBCUtil.escape(record.param); return update(update) > 0; } /** * Summarizes the reward data and purges expired rewards. */ public void purgeExpiredRewards () throws PersistenceException { executeUpdate(new Operation () { public Object invoke (Connection conn, DatabaseLiaison liaison) throws PersistenceException, SQLException { summarizeAndUpdate(conn, "", "ACTIVATIONS"); summarizeAndUpdate(conn, "and REDEEMER_IDENT is NOT NULL", "REDEMPTIONS"); return null; } }); update("delete from REWARD_RECORDS using REWARD_RECORDS, REWARD_INFO " + "where REWARD_INFO.REWARD_ID = REWARD_RECORDS.REWARD_ID " + "and REWARD_INFO.EXPIRATION <= NOW()"); } /** * Summarizes the reward data and stores it in the reward info records. */ protected void summarizeAndUpdate (Connection conn, String condition, String column) throws PersistenceException, SQLException { String activatedQuery = "select count(*), REWARD_INFO.REWARD_ID " + "from REWARD_RECORDS, REWARD_INFO " + "where REWARD_INFO.REWARD_ID = REWARD_RECORDS.REWARD_ID " + condition + " group by REWARD_ID"; String activatedUpdate = "update REWARD_INFO set " + column + " = ? where REWARD_ID = ?"; PreparedStatement ustmt = null; Statement stmt = null; try { stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(activatedQuery); ustmt = conn.prepareStatement(activatedUpdate); while (rs.next()) { ustmt.setInt(1, rs.getInt(1)); ustmt.setInt(2, rs.getInt(2)); ustmt.executeUpdate(); } } finally { JDBCUtil.close(ustmt); JDBCUtil.close(stmt); } } /** * Returns the max userid currently in use. */ protected int getMaxUserId () throws PersistenceException { return execute(new Operation () { public Integer invoke (Connection conn, DatabaseLiaison liaison) throws PersistenceException, SQLException { String query = "select MAX(userId) from users"; Statement stmt = null; int maxUserId = 0; try { stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query); if (rs.next()) { maxUserId = rs.getInt(1); } else { log.warning("No users found!?"); } } finally { JDBCUtil.close(stmt); } return maxUserId; } }); } @Override protected void migrateSchema (Connection conn, DatabaseLiaison liaison) throws SQLException, PersistenceException { String[] rewardInfoTable = { "REWARD_ID INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY", "DESCRIPTION VARCHAR(255) NOT NULL", "COUPON_CODE VARCHAR(255)", "DATA VARCHAR(255) NOT NULL", "EXPIRATION DATE NOT NULL", "MAX_ELIGIBLE_ID INTEGER NOT NULL", "ACTIVATIONS INTEGER NOT NULL", "REDEMPTIONS INTEGER NOT NULL" }; JDBCUtil.createTableIfMissing(conn, "REWARD_INFO", rewardInfoTable, ""); // TEMP: (2006-12-22) add COUPON_CODE column JDBCUtil.addColumn(conn, "REWARD_INFO", "COUPON_CODE", "VARCHAR(255)", "DESCRIPTION"); // END TEMP String[] rewardsTable = { "REWARD_ID INTEGER NOT NULL", "ACCOUNT VARCHAR(255)", "REDEEMER_IDENT VARCHAR(64)", "PARAM VARCHAR(255)", "PRIMARY KEY (REWARD_ID, ACCOUNT, PARAM)", "INDEX (ACCOUNT)", "INDEX (REDEEMER_IDENT)" }; JDBCUtil.createTableIfMissing(conn, "REWARD_RECORDS", rewardsTable, ""); // Add a parameter column. This allows a particular reward to be granted more than once, and // pass specific details to the reward handler. if (!JDBCUtil.tableContainsColumn(conn, "REWARD_RECORDS", "PARAM")) { // unfortunately, we need to recreate the primary key JDBCUtil.dropPrimaryKey(conn, "REWARD_RECORDS"); // add generic parameter column, and include JDBCUtil.addColumn(conn, "REWARD_RECORDS", "PARAM", "VARCHAR(255), " + "ADD PRIMARY KEY (REWARD_ID, ACCOUNT, PARAM)", null); } } @Override protected void createTables () { _infoTable = new Table( RewardInfo.class, "REWARD_INFO", REWARD_INFO_PRIMARY_KEY, true); _rewardsTable = new Table( RewardRecord.class, "REWARD_RECORDS", REWARD_RECORDS_PRIMARY_KEYS, true); } /** The primary key for the reward info table. */ protected static final String REWARD_INFO_PRIMARY_KEY = "REWARD_ID"; /** The primary keys for the rewards table. */ protected static final String[] REWARD_RECORDS_PRIMARY_KEYS = { "REWARD_ID", "ACCOUNT", "PARAM" }; /** The table used to store reward information. */ protected Table _infoTable; /** The table used to store activated rewards. */ protected Table _rewardsTable; }