package com.jbidwatcher.auction;
/*
* Copyright (c) 2000-2007, CyberFOX Software, Inc. All Rights Reserved.
*
* Developed by mrs (Morgan Schweers)
*/
import com.jbidwatcher.util.*;
import com.jbidwatcher.auction.event.EventLogger;
import com.jbidwatcher.util.Currency;
import com.jbidwatcher.util.Observer;
import com.jbidwatcher.util.config.*;
import com.jbidwatcher.util.queue.MQFactory;
import com.jbidwatcher.util.db.Table;
import java.util.*;
/**
* @brief Contains all the methods to examine, control, and command a
* specific auction.
*
* Where the AuctionInfo class contains information which is purely
* retrieved from the server, the AuctionEntry class decorates that
* with things like when it was last updated, whether to snipe, any
* comment the user might have made on it, etc.
*
* I.e. AuctionEntry keeps track of things that the PROGRAM needs to
* know about the auction, not things that are inherent to auctions.
*
* This is not descended from AuctionInfo because the actual type of
* AuctionInfo varies per server.
*
* @author Morgan Schweers
* @see AuctionInfo
* @see SpecificAuction
*/
public class AuctionEntry extends AuctionCore implements Comparable<AuctionEntry>, EntryInterface {
private Category mCategory;
private Presenter mAuctionEntryPresenter = null;
public boolean equals(Object o) {
return o instanceof AuctionEntry && compareTo((AuctionEntry) o) == 0;
}
/**
* @brief Set a status message, and mark that the connection is currently invalid.
*/
public void logError() {
setLastStatus("Communications failure talking to the server.");
setInvalid();
}
public Currency bestValue() {
if (isSniped()) {
return getSnipe().getAmount();
}
return isBidOn() && !isComplete() ? getBid() : getCurrentPrice();
}
public Currency getSnipeAmount() {
return isSniped() ? getSnipe().getAmount() : Currency.NoValue();
}
public int getSnipeQuantity() {
return isSniped() ? getSnipe().getQuantity() : 0;
}
AuctionSnipe getSnipe() {
if(mSnipe == null) {
if(get("snipe_id") != null) {
mSnipe = AuctionSnipe.find(get("snipe_id"));
if(mSnipe == null) {
// Couldn't find the snipe in the database.
setInteger("snipe_id", null);
saveDB();
}
}
}
return mSnipe;
}
/**
* A logging class for keeping track of events.
*
* @see com.jbidwatcher.auction.event.EventLogger
*/
private EventLogger mEntryEvents = null;
/**
* Have we ever obtained this auction data from the server?
*/
private boolean mLoaded =false;
private AuctionSnipe mSnipe = null;
/**
* How much was a cancelled snipe for? (Recordkeeping)
*/
private Currency mCancelSnipeBid = null;
/**
* What AuctionServer is responsible for handling this
* AuctionEntry's actions?
*/
private AuctionServerInterface mServer = null;
/**
* Delta in time from the end of the auction that sniping will
* occur at. It's possible to set a different snipe time for each
* auction, although it's not presently implemented through any UI.
*/
private long mSnipeAt = -1;
/**
* Default delta in time from the end of the auction that sniping
* will occur at. This valus can be read and modified by
* getDefaultSnipeTime() & setDefaultSnipeTime().
*/
private static long sDefaultSnipeAt = Constants.THIRTY_SECONDS;
private StringBuffer mLastErrorPage = null;
/**
* Does all the jobs of the constructors, so that the constructors
* become simple calls to this function. Presets up all the
* necessary variables, loads any data in, sets the lastUpdated
* flag, all the timers, retrieves the auction if necessary.
*
* @param auctionIdentifier - Each auction site has an identifier that
* is used to key the auction.
*/
private synchronized void prepareAuctionEntry(String auctionIdentifier) {
AuctionInfo info = mServer.create(auctionIdentifier);
mLoaded = info != null;
if(mLoaded) {
setString("identifier", auctionIdentifier);
info.saveDB();
setInteger("auction_id", info.getId());
}
/**
* Note that a bad auction (couldn't get an auction server, or a
* specific auction info object) doesn't have an identifier, and
* isn't loaded. This will fail out the init process, and this
* will never be added to the items list.
*/
if (mLoaded) {
Currency currentPrice = info.getBestPrice();
setDate("last_updated_at", new Date());
setDefaultCurrency(currentPrice);
saveDB();
notifyObservers(ObserverMode.AFTER_CREATE);
updateHighBid();
checkHighBidder();
checkEnded();
}
}
///////////////
// Constructor
/** Construct an AuctionEntry from just the ID, loading all necessary info
* from the server.
*
* @param auctionIdentifier The auction ID, from which the entire
* AuctionEntry is built by loading data from the server.
* @param server - The auction server for this entry.
*/
private AuctionEntry(String auctionIdentifier, AuctionServerInterface server) {
mServer = server;
checkConfigurationSnipeTime();
prepareAuctionEntry(auctionIdentifier);
}
/**
* A constructor that does almost nothing. This is to be used
* for ActiveRecord, which fills this out when pulling from a database record.
* <p/>
* Uses the default server.
*/
public AuctionEntry() {
checkConfigurationSnipeTime();
notifyObservers(ObserverMode.AFTER_CREATE);
}
public boolean hasAuction() {
AuctionInfo ai = AuctionInfo.findByIdOrIdentifier(getAuctionId(), getIdentifier());
return (ai != null);
}
public enum ObserverMode { AFTER_CREATE, AFTER_SAVE }
private static List<Observer<AuctionEntry>> allObservers = new ArrayList<Observer<AuctionEntry>>();
private void notifyObservers(ObserverMode event) {
for(Observer<AuctionEntry> toNotify : allObservers) {
switch (event) {
case AFTER_CREATE: {
toNotify.afterCreate(this);
break;
}
case AFTER_SAVE: {
toNotify.afterSave(this);
}
}
}
}
public static void addObserver(Observer<AuctionEntry> observer) {
allObservers.add(observer);
}
/**
* Create a new auction entry for the ID passed in. If it is in the deleted list, or already exists in
* the database, it will return null.
*
* @param identifier - The auction identifier to create an auction for.
*
* @return - null if the auction is in the deleted entry table, or the existing auction
* entry table, otherwise returns a valid AuctionEntry for the auction identifier provided.
*/
static AuctionEntry construct(String identifier, AuctionServerInterface server) {
AuctionEntry ae = new AuctionEntry(identifier, server);
if (ae.isLoaded()) {
String id = ae.saveDB();
if (id != null) {
JConfig.increment("stats.auctions");
return ae;
}
}
return null;
}
static AuctionEntry construct(AuctionServerInterface server) {
AuctionEntry ae = new AuctionEntry();
ae.setServer(server);
return ae;
}
/**
* @brief Look up to see if the auction is ended yet, just sets
* mComplete if it is.
*/
private void checkEnded() {
if(!isComplete()) {
Date serverTime = new Date(System.currentTimeMillis() +
getServer().getServerTimeDelta());
// If we're past the end time, update once, and never again.
if(serverTime.after(getEndDate())) {
setComplete(true);
setNeedsUpdate();
}
}
}
/////////////
// Accessors
/**
* @brief Return the server associated with this entry.
*
* @return The server that this auction entry is associated with.
*/
public AuctionServerInterface getServer() {
return(mServer);
}
/**
* @brief Set the auction server for this entry.
*
* First, if there are any snipes in the 'old' server, cancel them.
* Then set the server to the passed in value.
* Then re-set up any snipes associated with the listing.
*
* @param newServer - The server to associate with this auction entry.
*/
public void setServer(AuctionServerInterface newServer) {
//noinspection ObjectEquality
if(newServer != mServer) {
// "CANCEL_SNIPE #{id}"
if(isSniped()) getServer().cancelSnipe(getIdentifier());
mServer = newServer;
if(isSniped()) getServer().setSnipe(getIdentifier());
}
}
/**
* @brief Query whether this entry has ever been loaded from the server.
*
* Really shouldn't be necessary, but is. If we try to create an
* AuctionEntry with a bad identifier, that doesn't match any
* server, or isn't 'live' on the auction server, we need an error
* of this sort, to identify that the load failed. This is mainly
* because constructors don't fail.
*
* @return Whether this entry has ever been loaded from the server.
*/
private boolean isLoaded() { return(mLoaded); }
/**
* @brief Check if the current snipe value would be a valid bid currently.
*
* @return true if the current snipe is at least one minimum bid
* increment over the current high bid. Returns false otherwise.
*/
public boolean isSnipeValid() {
if(getSnipe() == null) return false;
Currency minIncrement = getServer().getMinimumBidIncrement(getCurBid(), getNumBidders());
boolean rval = false;
try {
if(getSnipe().getAmount().getValue() >= getCurBid().add(minIncrement).getValue()) {
rval = true;
}
} catch(Currency.CurrencyTypeException cte) {
JConfig.log().handleException("This should never happen (" + getCurBid() + ", " + minIncrement + ", " + getSnipe().getAmount() + ")!", cte);
}
return rval;
}
/**
* @brief Check if the user has an outstanding snipe on this auction.
*
* @return Whether there is a snipe waiting on this auction.
*/
public boolean isSniped() {
return getSnipe() != null;
}
/**
* @brief Check if the user has ever placed a bid (or completed
* snipe) on this auction.
*
* @return Whether the user has ever actually submitted a bid to the
* server for this auction.
*/
public boolean isBidOn() { return(getBid() != null && !getBid().isNull()); }
/**
* @brief Check if the current user is the high bidder on this
* auction.
*
* This should eventually handle multiple users per server, so that
* users can have multiple identities per auction site.
*
* @return Whether the current user is the high bidder.
*/
public boolean isHighBidder() { return isWinning(); }
public boolean isWinning() { return getBoolean("winning", false); }
public void setWinning(boolean state) { setBoolean("winning", state); }
/**
* @brief Check if the current user is the seller for this auction.
*
* This should eventually handle multiple users per server, so that
* users can have multiple identities per auction site.
* FUTURE FEATURE -- mrs: 02-January-2003 01:25
*
* @return Whether the current user is the seller.
*/
public boolean isSeller() { return getServer().isCurrentUser(getSellerName()); }
/**
* @brief What was the highest amount actually submitted to the
* server as a bid?
*
* With some auction servers, it might be possible to find out how
* much the user bid, but in general presume this value is only set
* by bidding through this program, or firing a snipe.
*
* @return The highest amount bid through this program.
*/
public Currency getBid() { return getMonetary("last_bid_amount"); }
/**
* @brief Set the highest amount actually submitted to the server as a bid.
* What is the maximum amount the user bid on the last time they bid?
*
* @param highBid - The new high bid value to set for this auction.
*/
public void setBid(Currency highBid) {
setMonetary("last_bid_amount", highBid == null ? Currency.NoValue() : highBid);
saveDB();
}
public void setBidQuantity(int quant) {
setInteger("last_bid_quantity", quant);
saveDB();
}
/**
* @brief What was the most recent number of items actually
* submitted to the server as part of a bid?
* How many items were bid on the last time the user bid?
*
* @return The count of items bid on the last time a user bid.
*/
public int getBidQuantity() {
if(isBidOn()) {
Integer i = getInteger("last_bid_quantity");
return i != null ? i : 1;
}
return 0;
}
/**
* @brief Get the default snipe time as configured.
*
* @return - The default snipe time from the configuration. If it's
* not set, return a standard 30 seconds.
*/
private static long getGlobalSnipeTime() {
long snipeTime;
String strConfigSnipeAt = JConfig.queryConfiguration("snipemilliseconds");
if(strConfigSnipeAt != null) {
snipeTime = Long.parseLong(strConfigSnipeAt);
} else {
snipeTime = Constants.THIRTY_SECONDS;
}
return snipeTime;
}
/**
* @brief Check if the configuration has a 'snipemilliseconds'
* entry, and update the default if it does.
*/
private static void checkConfigurationSnipeTime() {
sDefaultSnipeAt = getGlobalSnipeTime();
}
/**
* @brief Set how long before auctions are complete to fire snipes
* for any auction using the default snipe timer.
*
* @param newSnipeAt - The number of milliseconds prior to the end
* of auctions that the snipe timer will fire. Can be overridden by
* setSnipeTime() on a per-auction basis.
*/
public static void setDefaultSnipeTime(long newSnipeAt) {
sDefaultSnipeAt = newSnipeAt;
}
public long getSnipeTime() {
return hasDefaultSnipeTime()? sDefaultSnipeAt : mSnipeAt;
}
public boolean hasDefaultSnipeTime() {
return(mSnipeAt == -1);
}
public void setSnipeTime(long newSnipeTime) {
mSnipeAt = newSnipeTime;
}
/**
* @brief Get the time when this entry will no longer be considered
* 'newly added', or null if it's been cleared, or is already past.
*
* @return The time at which this entry is no longer new.
*/
public boolean isJustAdded() {
Date d = getDate("created_at");
return (d != null) && (d.getTime() > (System.currentTimeMillis() - (Constants.ONE_MINUTE * 5)));
}
///////////////////////////
// Actual logic functions
public void updateHighBid() {
int numBidders = getNumBidders();
if (numBidders > 0 || isFixed()) {
getServer().updateHighBid(getIdentifier());
}
}
/**
* @brief On update, we check if we're the high bidder.
*
* When you change user ID's, you should force a complete update, so
* this is synchronized correctly.
*/
private void checkHighBidder() {
int numBidders = getNumBidders();
if(numBidders > 0) {
if(isBidOn() && isPrivate()) {
Currency curBid = getCurBid();
try {
if(curBid.less(getBid())) setWinning(true);
} catch(Currency.CurrencyTypeException cte) {
/* Should never happen...? */
JConfig.log().handleException("This should never happen (bad Currency at this point!).", cte);
}
if(curBid.equals(getBid())) {
setWinning(numBidders == 1);
// winning == false means that there are multiple bidders, and the price that
// two (this user, and one other) bid are exactly the same. How
// do we know who's first, given that it's a private auction?
//
// The only answer I have is to presume that we're NOT first.
// eBay knows the 'true' answer, but how to extract it from them...
}
} else {
setWinning(getServer().isCurrentUser(getHighBidder()));
}
}
}
////////////////////////////
// Periodic logic functions
/**
* @brief Mark this entry as being not-invalid.
*/
public void clearInvalid() {
setBoolean("invalid", false);
saveDB();
}
/**
* @brief Mark this entry as being invalid for some reason.
*/
public void setInvalid() {
setBoolean("invalid", true);
saveDB();
}
/**
* @brief Is this entry invalid for any reason?
*
* Is the data reasonably synchronized with the server? (When the
* site stops providing the data, or an error occurs when retrieving
* this auction, this will be true.)
*
* @return - True if this auction is considered invalid, false if it's okay.
*/
public boolean isInvalid() {
return getBoolean("invalid", false);
}
/**
* @brief Store a user-specified comment about this item.
* Allow the user to add a personal comment about this auction.
*
* @param newComment - The comment to keep track of. If it's empty,
* we effectively delete the comment.
*/
public void setComment(String newComment) {
if(newComment.trim().length() == 0)
setString("comment", null);
else
setString("comment", newComment.trim());
saveDB();
}
/**
* @brief Get any user-specified comment regarding this auction.
*
* @return Any comment the user may have stored about this item.
*/
public String getComment() {
return getString("comment");
}
/**
* @brief Add an auction-specific status message into its own event log.
*
* @param inStatus - A string that explains what the event is.
*/
public void setLastStatus(String inStatus) {
getEvents().setLastStatus(inStatus);
}
public void setShipping(Currency newShipping) {
setMonetary("shipping", newShipping);
saveDB();
}
/**
* @brief Get a plain version of the event list, where each line is
* a seperate event, including the title and identifier.
*
* @return A string with all the event information included.
*/
public String getLastStatus() { return getEvents().getLastStatus(); }
/**
* @brief Get either a plain version of the events, or a complex
* (bulk) version which doesn't include the title and identifier,
* since those are set by the AuctionEntry itself, and are based
* on its own data.
*
* @return A string with all the event information included.
*/
public String getStatusHistory() {
return getEvents().getAllStatuses();
}
public int getStatusCount() {
return getEvents().getStatusCount();
}
private EventLogger getEvents() {
if(mEntryEvents == null) mEntryEvents = new EventLogger(getIdentifier(), getId(), getTitle());
return mEntryEvents;
}
/////////////////////
// Sniping functions
/**
* @brief Return whether this entry ever had a snipe cancelled or not.
*
* @return - true if a snipe was cancelled, false otherwise.
*/
public boolean snipeCancelled() { return mCancelSnipeBid != null; }
/**
* @brief Return the amount that the snipe bid was for, before it
* was cancelled.
*
* @return - A currency amount that was set to snipe, but cancelled.
*/
public Currency getCancelledSnipe() { return mCancelSnipeBid; }
/**
* Cancel the snipe and clear the multisnipe setting. This is used for
* user-driven snipe cancellations, and errors like the listing going away.
*
* @param after_end - Is this auction already completed?
*/
public void cancelSnipe(boolean after_end) {
handleCancel(after_end);
// If the multisnipe was null, remove the snipe entirely.
prepareSnipe(Currency.NoValue(), 0);
setInteger("multisnipe_id", null);
saveDB();
}
private void handleCancel(boolean after_end) {
if(isSniped()) {
JConfig.log().logDebug("Cancelling Snipe for: " + getTitle() + '(' + getIdentifier() + ')');
setLastStatus("Cancelling snipe.");
if(after_end) {
setBoolean("auto_canceled", true);
mCancelSnipeBid = getSnipe().getAmount();
}
}
}
public void snipeCompleted() {
setSnipedAmount(getSnipe().getAmount());
setBid(getSnipe().getAmount());
setBidQuantity(getSnipe().getQuantity());
getSnipe().delete();
setInteger("snipe_id", null);
mSnipe = null;
setDirty();
setNeedsUpdate();
saveDB();
}
private void setSnipedAmount(Currency amount) {
setMonetary("sniped_amount", amount == null ? Currency.NoValue() : amount);
}
/**
* In this case, the snipe failed, and we want to cancel the snipe, but we
* don't want to remove the listing from the multisnipe group, in case you
* still win it. (For example, if you have a bid on it already.)
*/
public void snipeFailed() {
handleCancel(true);
setDirty();
setNeedsUpdate();
saveDB();
}
/**
* @brief Completely update auction info from the server for this auction.
*/
public void update() {
setDate("last_updated_at", new Date());
// We REALLY don't want to leave an auction in the 'updating'
// state. It does bad things.
try {
getServer().reload(getIdentifier());
} catch(Exception e) {
JConfig.log().handleException("Unexpected exception during auction reload/update.", e);
}
try {
updateHighBid();
checkHighBidder();
} catch(Exception e) {
JConfig.log().handleException("Unexpected exception during high bidder check.", e);
}
if (isComplete()) {
onComplete();
} else {
long now = System.currentTimeMillis() + getServer().getServerTimeDelta();
Date serverTime = new Date(now);
if(now > getEndDate().getTime())
// If we're past the end time, update once, and never again.
if (serverTime.after(getEndDate())) {
setComplete(true);
setNeedsUpdate();
}
}
saveDB();
}
private void onComplete() {
boolean won = isHighBidder() && (!isReserve() || isReserveMet());
if (won) {
JConfig.increment("stats.won");
MQFactory.getConcrete("won").enqueue(getIdentifier());
// Metrics
if(getBoolean("was_sniped")) {
JConfig.getMetrics().trackEvent("snipe", "won");
} else {
JConfig.getMetrics().trackEvent("auction", "won");
}
} else {
MQFactory.getConcrete("notwon").enqueue(getIdentifier());
// Metrics
if (getBoolean("was_sniped")) {
JConfig.getMetrics().trackEvent("snipe", "lost");
} else {
if(isBidOn()) {
JConfig.getMetrics().trackEvent("auction", "lost");
}
}
}
if (isSniped()) {
// It's okay to cancel the snipe here; if the auction was won, it would be caught above.
setLastStatus("Cancelling snipe, auction is reported as ended.");
cancelSnipe(true);
}
}
public void prepareSnipe(Currency snipe) { prepareSnipe(snipe, 1); }
/**
* @brief Set up the fields necessary for a future snipe.
*
* This needs to be enhanced to work with multiple items, and
* different snipe times.
*
* @param snipe The amount of money the user wishes to bid at the last moment.
* @param quantity The number of items they want to snipe for.
*/
public void prepareSnipe(Currency snipe, int quantity) {
if(snipe == null || snipe.isNull()) {
if(getSnipe() != null) {
getSnipe().delete();
}
setInteger("snipe_id", null);
mSnipe = null;
getServer().cancelSnipe(getIdentifier());
} else {
mSnipe = AuctionSnipe.create(snipe, quantity, 0);
getServer().setSnipe(getIdentifier());
}
setDirty();
saveDB();
MQFactory.getConcrete("Swing").enqueue("SNIPECHANGED");
}
/**
* @brief Refresh the snipe, so it picks up a potentially changed end time, or when initially loading items.
*/
public void refreshSnipe() {
getServer().setSnipe(getIdentifier());
}
/**
* @brief Bid a given price on an arbitrary number of a particular item.
*
* @param bid - The amount of money being bid.
* @param bidQuantity - The number of items being bid on.
*
* @return The result of the bid attempt.
*/
public int bid(Currency bid, int bidQuantity) {
setBid(bid);
setBidQuantity(bidQuantity);
setDate("last_bid_at", new Date(System.currentTimeMillis()));
JConfig.log().logDebug("Bidding " + bid + " on " + bidQuantity + " item[s] of (" + getIdentifier() + ")-" + getTitle());
int rval = getServer().bid(getIdentifier(), bid, bidQuantity);
saveDB();
return rval;
}
/**
* @brief Buy an item directly.
*
* @param quant - The number of them to buy.
*
* @return The result of the 'Buy' attempt.
*/
public int buy(int quant) {
int rval = AuctionServerInterface.BID_ERROR_NOT_BIN;
Currency bin = getBuyNow();
if(bin != null && !bin.isNull()) {
setBid(getBuyNow());
setBidQuantity(quant);
setDate("last_bid_at", new Date(System.currentTimeMillis()));
JConfig.log().logDebug("Buying " + quant + " item[s] of (" + getIdentifier() + ")-" + getTitle());
rval = getServer().buy(getIdentifier(), quant);
// Metrics
if(rval == AuctionServerInterface.BID_BOUGHT_ITEM) {
JConfig.getMetrics().trackEvent("buy", "success");
} else {
JConfig.getMetrics().trackEventValue("buy", "fail", Integer.toString(rval));
}
saveDB();
}
return rval;
}
/**
* @brief This auction entry needs to be updated.
*/
public void setNeedsUpdate() { setDate("last_updated_at", null); saveDB(); }
public Date getLastUpdated() { return getDate("last_updated_at"); }
/**
* @brief Get the category this belongs in, usually used for tab names, and fitting in search results.
*
* @return - A category, or null if none has been assigned.
*/
public String getCategory() {
if(mCategory == null) {
String category_id = get("category_id");
if(category_id != null) {
mCategory = Category.findFirstBy("id", category_id);
}
}
if(mCategory == null) {
setCategory(!isComplete() ? (isSeller() ? "selling" : "current") : "complete");
}
return mCategory != null ? mCategory.getName() : null;
}
/**
* @brief Set the category associated with the auction entry. If the
* auction is ended, this is automatically considered sticky.
*
* @param newCategory - The new category to associate this item with.
*/
public void setCategory(String newCategory) {
Category c = Category.findFirstByName(newCategory);
if(c == null) {
c = Category.findOrCreateByName(newCategory);
}
setInteger("category_id", c.getId());
mCategory = c;
if(isComplete()) setSticky(true);
saveDB();
}
/**
* @brief Returns whether or not this auction entry is 'sticky', i.e. sticks to any category it's set to.
* Whether the 'category' information is sticky (i.e. overrides 'deleted', 'selling', etc.)
*
* @return true if the entry is sticky, false otherwise.
*/
public boolean isSticky() { return getBoolean("sticky"); }
/**
* @brief Set the sticky flag on or off.
*
* This'll probably be exposed to the user through a right-click context menu, so that people
* can make auctions not move from their sorted categories when they end.
*
* @param beSticky - Whether or not this entry should be sticky.
*/
public void setSticky(boolean beSticky) {
if(beSticky != getBoolean("sticky")) {
setBoolean("sticky", beSticky);
saveDB();
}
}
public static final String endedAuction = "Auction ended.";
//0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}
/**
* @brief Determine the amount of time left, and format it prettily.
*
* @return A nicely formatted string showing how much time is left
* in this auction.
*/
public String getTimeLeft() {
long rightNow = System.currentTimeMillis();
long officialDelta = getServer().getServerTimeDelta();
long pageReqTime = getServer().getPageRequestTime();
if(!isComplete()) {
long dateDiff;
try {
dateDiff = getEndDate().getTime() - ((rightNow + officialDelta) - pageReqTime);
} catch(Exception endDateException) {
JConfig.log().handleException("Error getting the end date.", endDateException);
dateDiff = 0;
}
String mf = TimeLeftBuilder.getTimeLeftString(dateDiff);
if (mf != null) return mf;
}
return endedAuction;
}
/**
* For display during updates, we want the title and potentially the
* comment, to display all that in the status bar while we're
* updating.
*
* @return - A string containing the title alone, if no comment, or
* in the format: "title (comment)" otherwise.
*/
public String getTitleAndComment() {
String curComment = getComment();
if (curComment == null) return getTitle();
return getTitle() + " (" + curComment + ')';
}
@Override
public int hashCode() {
return getIdentifier().hashCode() ^ getEndDate().hashCode();
}
/**
* @brief Do a 'standard' compare to another AuctionEntry object.
*
* The standard ordering is as follows:
* (if identifiers or pointers are equal, entries are equal)
* If this end date is after the passed in one, we are greater.
* If this end date is before, we are lesser.
* Otherwise (EXACTLY equal dates!), order by identifier.
*
* @param other - The AuctionEntry to compare to.
*
* @return - -1 for lesser, 0 for equal, 1 for greater.
*/
public int compareTo(AuctionEntry other) {
// We are always greater than null
if(other == null) return 1;
// We are always equal to ourselves
//noinspection ObjectEquality
if(other == this) return 0;
String identifier = getIdentifier();
// If the identifiers are the same, we're equal.
if(identifier != null && identifier.equals(other.getIdentifier())) return 0;
final Date myEndDate = getEndDate();
final Date otherEndDate = other.getEndDate();
if(myEndDate == null && otherEndDate != null) return 1;
if(myEndDate != null) {
if(otherEndDate == null) return -1;
// If this ends later than the passed in object, then we are 'greater'.
if(myEndDate.after(otherEndDate)) return 1;
if(otherEndDate.after(myEndDate)) return -1;
}
// Whoops! Dates are equal, down to the second probably, or both null...
// If this has a null identifier, we're lower.
if (identifier == null) {
if (other.getIdentifier() != null) return -1;
return 0;
}
// At this point, we know identifier != null, so if the compared entry
// has a null identifier, we sort higher.
if(other.getIdentifier() == null) return 1;
// Since this ends exactly at the same time as another auction,
// check the identifiers (which *must* be different here.
return getIdentifier().compareTo(other.getIdentifier());
}
/**
* @brief Return a value that indicates the status via bitflags, so that sorted groups by status will show up grouped together.
*
* @return - An integer containing a bitfield of relevant status bits.
*/
public int getFlags() {
int r_flags = 1;
if (isFixed()) r_flags = 0;
if (getHighBidder() != null) {
if (isHighBidder()) {
r_flags = 2;
} else if (isSeller() && getNumBidders() > 0 &&
(!isReserve() || isReserveMet())) {
r_flags = 4;
}
}
if (!getBuyNow().isNull()) {
r_flags += 8;
}
if (isReserve()) {
if (isReserveMet()) {
r_flags += 16;
} else {
r_flags += 32;
}
}
if(hasPaypal()) r_flags += 64;
return r_flags;
}
private static AuctionInfo sAuction = new AuctionInfo();
@SuppressWarnings({"ObjectEquality"})
public boolean isNullAuction() { return get("auction_id") == null; }
private boolean deleting = false;
public AuctionInfo getAuction() {
String identifier = getString("identifier");
String auctionId = getString("auction_id");
AuctionInfo info = AuctionInfo.findByIdOrIdentifier(auctionId, identifier);
if(info == null) {
if(!deleting) {
deleting = true;
this.delete();
}
return sAuction;
}
boolean dirty = false;
if (!getDefaultCurrency().equals(info.getDefaultCurrency())) {
setDefaultCurrency(info.getDefaultCurrency());
dirty = true;
}
if (getString("identifier") == null) {
setString("identifier", info.getIdentifier());
dirty = true;
}
if (auctionId == null || !auctionId.equals(info.get("id"))) {
setInteger("auction_id", info.getId());
dirty = true;
}
if (dirty) {
saveDB();
}
return info;
}
protected void loadSecondary() {
AuctionInfo ai = AuctionInfo.findByIdOrIdentifier(getAuctionId(), getIdentifier());
if(ai != null) setAuctionInfo(ai);
}
/**
* @brief Force this auction to use a particular set of auction
* information for it's core data (like seller's name, current high
* bid, etc.).
*
* @param inAI - The AuctionInfo object to make the new core data. Must not be null.
*/
public void setAuctionInfo(AuctionInfo inAI) {
if (inAI.getId() != null) {
setSecondary(inAI.getBacking());
setDefaultCurrency(inAI.getDefaultCurrency());
setInteger("auction_id", inAI.getId());
setString("identifier", inAI.getIdentifier()); //?
checkHighBidder();
checkEnded();
saveDB();
}
}
////////////////////////////////////////
// Passthrough functions to AuctionInfo
/**
* Check current price, and fall back to buy-now price if 'current' isn't set.
*
* @return - The current price, or the buy now if current isn't set.
*/
public Currency getCurrentPrice() {
Currency curPrice = getCurBid();
if (curPrice == null || curPrice.isNull()) return getBuyNow();
return curPrice;
}
public Currency getCurrentUSPrice() {
Currency curPrice = getCurBid();
if (curPrice == null || curPrice.isNull()) return getBuyNowUS();
return getUSCurBid();
}
/**
* @return - Shipping amount, overrides AuctionInfo shipping amount if present.
*/
public String getSellerName() { return getAuction().getSellerName(); }
public Date getStartDate() {
Date start = super.getStartDate();
if(start != null) {
return start;
}
return Constants.LONG_AGO;
}
public Date getSnipeDate() { return new Date(getEndDate().getTime() - getSnipeTime()); }
public String getBrowseableURL() { return getServer().getBrowsableURLFromItem(getIdentifier()); }
public void setErrorPage(StringBuffer page) { mLastErrorPage = page; }
public StringBuffer getErrorPage() { return mLastErrorPage; }
public Currency getShippingWithInsurance() {
Currency ship = getShipping();
if(ship == null || ship.isNull())
return Currency.NoValue();
else {
ship = addInsurance(ship);
}
return ship;
}
private Currency addInsurance(Currency ship) {
if(getInsurance() != null &&
!getInsurance().isNull() &&
!isInsuranceOptional()) {
try {
ship = ship.add(getInsurance());
} catch(Currency.CurrencyTypeException cte) {
JConfig.log().handleException("Insurance is somehow a different type than shipping?!?", cte);
}
}
return ship;
}
public boolean isShippingOverridden() {
Currency ship = getMonetary("shipping");
return ship != null && !ship.isNull();
}
/**
* Is the auction deleted on the server?
*
* @return - true if the auction has been removed from the server, as opposed to deleted locally.
*/
public boolean isDeleted() {
return getBoolean("deleted", false);
}
/**
* Mark the auction as having been deleted by the auction server.
*
* Generally items are removed by the auction server because the listing is
* too old, violates some terms of service, the seller has been suspended,
* or the seller removed the listing themselves.
*/
public void setDeleted() {
if(!isDeleted()) {
setBoolean("deleted", true);
clearInvalid();
} else {
setComplete(true);
}
saveDB();
}
/**
* Mark the auction as NOT having been deleted by the auction server.
*
* It's possible we mistakenly saw a server-error as a 404 (or they
* presented it as such), so we need to be able to clear the deleted status.
*/
public void clearDeleted() {
if(isDeleted()) {
setBoolean("deleted", false);
saveDB();
}
}
public String getAuctionId() { return get("auction_id"); }
/**
* Set this auction as completed.
*
* Has this auction already ended? We keep track of this, so we
* don't waste time on it afterwards, even as much as creating a
* Date object, and comparing.
*/
public void setComplete(boolean complete) { setBoolean("ended", complete); saveDB(); }
/*************************/
/* Database access stuff */
/*************************/
public String saveDB() {
if(isNullAuction()) return null;
String auctionId = getAuctionId();
if(auctionId != null) set("auction_id", auctionId);
// This just makes sure we have a default category before saving.
getCategory();
if(mCategory != null) {
String categoryId = mCategory.saveDB();
if(categoryId != null) set("category_id", categoryId);
}
if(getSnipe() != null) {
String snipeId = getSnipe().saveDB();
if(snipeId != null) set("snipe_id", snipeId);
}
if(mEntryEvents != null) {
mEntryEvents.save();
}
String id = super.saveDB();
set("id", id);
notifyObservers(ObserverMode.AFTER_SAVE);
return id;
}
// public boolean reload() {
// try {
// AuctionEntry ae = EntryCorral.findFirstBy("id", get("id"));
// if (ae != null) {
// setBacking(ae.getBacking());
//
// AuctionInfo ai = AuctionInfo.findByIdOrIdentifier(getAuctionId(), getIdentifier());
// setAuctionInfo(ai);
//
// ae.getCategory();
// mCategory = ae.mCategory;
// mSnipe = ae.getSnipe();
// mEntryEvents = ae.getEvents();
// return true;
// }
// } catch (Exception e) {
// // Ignored - the reload semi-silently fails.
// JConfig.log().logDebug("reload from the database failed for (" + getIdentifier() + ")");
// }
// return false;
// }
protected Table getDatabase() {
return EntryTable.getRealDatabase();
}
public boolean delete() {
AuctionInfo ai = AuctionInfo.findByIdOrIdentifier(getAuctionId(), getIdentifier());
if(ai != null) ai.delete();
if(getSnipe() != null) getSnipe().delete();
return super.delete();
}
public Presenter getPresenter() {
return mAuctionEntryPresenter;
}
public void setNumBids(int bidCount) {
AuctionInfo info = AuctionInfo.findByIdOrIdentifier(getAuctionId(), getIdentifier());
info.setNumBids(bidCount);
info.saveDB();
}
public boolean isUpdateRequired() {
return getDate("last_updated_at") == null;
}
public String getUnique() {
return getIdentifier();
}
public void setPresenter(Presenter presenter) {
mAuctionEntryPresenter = presenter;
}
}