package com.jbidwatcher.auction;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.jbidwatcher.auction.event.EventStatus;
import com.jbidwatcher.util.Constants;
import com.jbidwatcher.util.HashBacked;
import com.jbidwatcher.util.config.JConfig;
import com.jbidwatcher.util.db.ActiveRecord;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;
/**
* User: mrs
* Date: Apr 26, 2009
* Time: 1:46:02 AM
*
* A single clearing house for auction entries, so everything operates on the
* same underlying objects. Thread safety is a serious concern.
*/
abstract class EntryCorralTemplate<T extends ActiveRecord> {
private Map<String, Reference<T>> mEntryList;
private final Map<String, Lock> mLockList;
protected EntryCorralTemplate() {
mEntryList = new HashMap<String, Reference<T>>();
mLockList = new HashMap<String, Lock>();
}
private T get(String identifier) {
Reference<T> r = mEntryList.get(identifier);
if(r != null) return r.get();
return null;
}
abstract public T getItem(String param);
public ActiveRecord takeForWrite(String identifier) {
T result = get(identifier);
if(result == null) {
result = getItem(identifier);
}
if(result != null) {
Lock l = mLockList.get(identifier);
if (l == null) {
l = new ReentrantLock(true);
synchronized (mLockList) {
mLockList.put(identifier, l);
}
}
l.lock();
}
return result;
}
public void release(String identifier) {
Lock l = mLockList.get(identifier);
if(l != null) l.unlock();
}
public T takeForRead(String identifier) {
T result = get(identifier);
if (result == null) {
result = getItem(identifier);
if(result != null) mEntryList.put(identifier, new WeakReference<T>(result));
}
return result;
}
public T put(T ae) {
T result = chooseLatest(ae, ae.getUnique());
mEntryList.put(ae.getUnique(), new SoftReference<T>(result));
return result;
}
public T putWeakly(T ae) {
return chooseLatest(ae, ae.getUnique());
}
protected T chooseLatest(T ae, String identifier) {
T chosen;
T existing = get(identifier);
final Date inputDate = ae.getDate("updated_at");
final Date existingDate = (existing == null ? null : existing.getDate("updated_at"));
if(existing == null ||
(inputDate != null && existingDate == null) ||
(inputDate != null && inputDate.after(existingDate))) {
if(mEntryList.get(identifier) instanceof SoftReference) {
mEntryList.put(identifier, new SoftReference<T>(ae));
} else {
mEntryList.put(identifier, new WeakReference<T>(ae));
}
chosen = ae;
} else {
chosen = existing;
}
return chosen;
}
public T erase(String identifier) {
synchronized(mLockList) {
Lock l = mLockList.remove(identifier);
Reference<T> rval = mEntryList.remove(identifier);
if (l != null) l.unlock();
if (rval == null) {
return null;
}
return rval.get();
}
}
public void clear() {
synchronized(mLockList) {
for (String s : mLockList.keySet()) {
erase(s);
}
mEntryList.clear();
}
}
}
@Singleton
public class EntryCorral extends EntryCorralTemplate<AuctionEntry> {
static final String snipeFinder = "(snipe_id IS NOT NULL OR multisnipe_id IS NOT NULL) AND (entries.ended != 1 OR entries.ended IS NULL)";
private static Date updateSince = new Date();
private static Date endingSoon = new Date();
private static Date hourAgo = new Date();
private static SimpleDateFormat mDateFormat = new SimpleDateFormat(HashBacked.DB_DATE_FORMAT);
// private static Table sDB = null;
public static AuctionEntry findFirstBy(String key, String value) {
return (AuctionEntry) ActiveRecord.findFirstBy(AuctionEntry.class, key, value);
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findActive() {
String notEndedQuery = "SELECT e.* FROM entries e JOIN auctions a ON a.id = e.auction_id WHERE (e.ended != 1 OR e.ended IS NULL) ORDER BY a.ending_at ASC";
return (List<AuctionEntry>) ActiveRecord.findAllBySQL(AuctionEntry.class, notEndedQuery);
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findEnded() {
return (List<AuctionEntry>) ActiveRecord.findAllBy(AuctionEntry.class, "ended", "1");
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findAllNeedingUpdates(long since) {
long timeRange = System.currentTimeMillis() - since;
updateSince.setTime(timeRange);
return (List<AuctionEntry>) ActiveRecord.findAllByPrepared(AuctionEntry.class,
"SELECT e.* FROM entries e" +
" JOIN auctions a ON a.id = e.auction_id" +
" WHERE (e.ended != 1 OR e.ended IS NULL)" +
" AND (e.last_updated_at IS NULL OR e.last_updated_at < ?)" +
" ORDER BY a.ending_at ASC", mDateFormat.format(updateSince));
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findEndingNeedingUpdates(long since) {
long timeRange = System.currentTimeMillis() - since;
updateSince.setTime(timeRange);
// Update more frequently in the last 25 minutes.
endingSoon.setTime(System.currentTimeMillis() + 25 * Constants.ONE_MINUTE);
hourAgo.setTime(System.currentTimeMillis() - Constants.ONE_HOUR);
return (List<AuctionEntry>) ActiveRecord.findAllByPrepared(AuctionEntry.class,
"SELECT e.* FROM entries e JOIN auctions a ON a.id = e.auction_id" +
" WHERE (e.last_updated_at IS NULL OR e.last_updated_at < ?)" +
" AND (e.ended != 1 OR e.ended IS NULL)" +
" AND a.ending_at < ? AND a.ending_at > ?" +
" ORDER BY a.ending_at ASC", mDateFormat.format(updateSince),
mDateFormat.format(endingSoon), mDateFormat.format(hourAgo));
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findAll() {
return (List<AuctionEntry>) ActiveRecord.findAllBySQL(AuctionEntry.class, "SELECT * FROM entries");
}
public static AuctionEntry nextSniped() {
String sql = "SELECT entries.* FROM entries, auctions WHERE " + snipeFinder +
" AND (entries.auction_id = auctions.id) ORDER BY auctions.ending_at ASC";
return (AuctionEntry) ActiveRecord.findFirstBySQL(AuctionEntry.class, sql);
}
/**
* Locate an AuctionEntry by first finding an AuctionInfo with the passed
* in auction identifier, and then looking for an AuctionEntry which
* refers to that AuctionInfo row.
*
* TODO EntryCorral callers? (Probably!)
*
* @param identifier - The auction identifier to search for.
* @return - null indicates that the auction isn't in the database yet,
* otherwise an AuctionEntry will be loaded and returned.
*/
public static AuctionEntry findByIdentifier(String identifier) {
AuctionEntry ae = (AuctionEntry)ActiveRecord.findFirstBy(AuctionEntry.class, "identifier", identifier);
AuctionInfo ai;
if(ae != null) {
ai = AuctionInfo.findByIdOrIdentifier(ae.getAuctionId(), identifier);
if(ai == null) {
JConfig.log().logMessage("Error loading auction #" + identifier + ", entry found, auction missing.");
ae = null;
}
}
if(ae == null) {
ai = AuctionInfo.findByIdOrIdentifier(null, identifier);
if(ai != null) {
ae = findFirstBy("auction_id", ai.getString("id"));
if (ae != null) ae.setAuctionInfo(ai);
}
}
return ae;
}
/**
* TODO: Clear from the entry corral?
* @param toDelete List of AuctionEntries to delete from the database.
*
* @return true if the entries were all deleted, or the list was empty; false if an error occurred during deletion.
*/
public static boolean deleteAll(List<AuctionEntry> toDelete) {
if(toDelete.isEmpty()) return true;
String entries = ActiveRecord.makeCommaList(toDelete);
List<Integer> auctions = new ArrayList<Integer>();
List<AuctionSnipe> snipes = new ArrayList<AuctionSnipe>();
for(AuctionEntry entry : toDelete) {
auctions.add(entry.getInteger("auction_id"));
if(entry.isSniped()) snipes.add(entry.getSnipe());
}
boolean success = new EventStatus().deleteAllEntries(entries);
if(!snipes.isEmpty()) success &= AuctionSnipe.deleteAll(snipes);
success &= AuctionInfo.deleteAll(auctions);
success &= EntryTable.getRealDatabase().deleteBy("id IN (" + entries + ")");
return success;
}
public static int countByCategory(Category c) {
if(c == null) return 0;
return EntryTable.getRealDatabase().countBySQL("SELECT COUNT(*) FROM entries WHERE category_id=" + c.getId());
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findManualUpdates() {
return (List<AuctionEntry>) ActiveRecord.findAllBySQL(AuctionEntry.class, "SELECT e.* FROM entries e JOIN auctions a ON a.id = e.auction_id WHERE e.last_updated_at IS NULL ORDER BY a.ending_at ASC");
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findRecentlyEnded(int itemCount) {
return (List<AuctionEntry>) ActiveRecord.findAllBySQL(AuctionEntry.class, "SELECT e.* FROM entries e JOIN auctions a ON a.id = e.auction_id WHERE e.ended = 1 ORDER BY a.ending_at DESC", itemCount);
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findEndingSoon(int itemCount) {
return (List<AuctionEntry>) ActiveRecord.findAllBySQL(AuctionEntry.class, "SELECT e.* FROM entries e JOIN auctions a ON a.id = e.auction_id WHERE (e.ended != 1 OR e.ended IS NULL) ORDER BY a.ending_at ASC", itemCount);
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findBidOrSniped(int itemCount) {
return (List<AuctionEntry>) ActiveRecord.findAllBySQL(AuctionEntry.class, "SELECT e.* FROM entries e JOIN auctions a ON a.id = e.auction_id WHERE (e.snipe_id IS NOT NULL OR e.multisnipe_id IS NOT NULL OR e.bid_amount IS NOT NULL) ORDER BY a.ending_at ASC", itemCount);
}
public static void forceUpdateActive() {
EntryTable.getRealDatabase().execute("UPDATE entries SET last_updated_at=NULL WHERE ended != 1 OR ended IS NULL");
}
public static void trueUpEntries() {
EntryTable.getRealDatabase().execute("UPDATE entries SET auction_id=(SELECT max(id) FROM auctions WHERE auctions.identifier=entries.identifier)");
EntryTable.getRealDatabase().execute("DELETE FROM entries e WHERE id != (SELECT max(id) FROM entries e2 WHERE e2.auction_id = e.auction_id)");
}
@SuppressWarnings({"unchecked"})
public static List<AuctionEntry> findAllBy(String column, String value) {
return (List<AuctionEntry>)ActiveRecord.findAllBy(AuctionEntry.class, column, value);
}
public static int count() {
return ActiveRecord.count(AuctionEntry.class);
}
public static int activeCount() {
return EntryTable.getRealDatabase().countBy("(ended != 1 OR ended IS NULL)");
}
public static int completedCount() {
return EntryTable.getRealDatabase().countBy("ended = 1");
}
public static int uniqueCount() {
return EntryTable.getRealDatabase().countBySQL("SELECT COUNT(DISTINCT(identifier)) FROM entries WHERE identifier IS NOT NULL");
}
public static int snipedCount() {
return EntryTable.getRealDatabase().countBy(snipeFinder);
}
@Override
public AuctionEntry getItem(String param) {
return findByIdentifier(param);
}
@SuppressWarnings({"unchecked"})
public List<AuctionEntry> findAllSniped() {
List<AuctionEntry> sniped = (List<AuctionEntry>) ActiveRecord.findAllBySQL(AuctionEntry.class, "SELECT * FROM " + EntryTable.getTableName() + " WHERE (snipe_id IS NOT NULL OR multisnipe_id IS NOT NULL)");
if (sniped != null) {
List<AuctionEntry> results = new ArrayList<AuctionEntry>();
for (AuctionEntry ae : sniped) {
results.add(chooseLatest(ae, ae.getIdentifier()));
}
return results;
}
return null;
}
public List<Snipeable> getMultisnipedByGroup(String multisnipeIdentifier) {
List<? extends Snipeable> entries = findAllBy("multisnipe_id", multisnipeIdentifier);
List<Snipeable> rval = new ArrayList<Snipeable>(entries.size());
for (Snipeable entry : entries) {
Snipeable ae = takeForRead(entry.getIdentifier());
if (!ae.isComplete()) rval.add(ae);
}
return rval;
}
@Inject
private EntryCorral() { super(); }
}