Fix parsing of high bidder/buyer information in fixed price listings. Improve date parsing, so that listings with the word 'end' someplace in the text title aren't dying. by mrs, 18 Aug, 2009 10:27 PM
Diff this changeset:
AuctionEntry.java
cyberfox 1    package com.jbidwatcher.auction;
cyberfox 2    /*
cyberfox 3     * Copyright (c) 2000-2007, CyberFOX Software, Inc. All Rights Reserved.
cyberfox 4     *
cyberfox 5     * Developed by mrs (Morgan Schweers)
cyberfox 6     */
cyberfox 7    
mrs      8    import com.jbidwatcher.util.Constants;
mrs      9    import com.jbidwatcher.util.Currency;
mrs      10   import com.jbidwatcher.util.StringTools;
mrs      11   import com.jbidwatcher.auction.event.EventLogger;
mrs      12   import com.jbidwatcher.auction.event.EventStatus;
mrs      13   import com.jbidwatcher.util.config.*;
mrs      14   import com.jbidwatcher.util.queue.MQFactory;
mrs      15   import com.jbidwatcher.util.db.ActiveRecord;
mrs      16   import com.jbidwatcher.util.db.Table;
mrs      17   import com.jbidwatcher.util.xml.XMLElement;
mrs      18   import com.jbidwatcher.util.xml.XMLInterface;
cyberfox 19   
mrs      20   import java.io.File;
mrs      21   import java.text.MessageFormat;
mrs      22   import java.util.*;
mrs      23   import java.net.InetAddress;
mrs      24   import java.net.UnknownHostException;
cyberfox 25   
cyberfox 26   /**
cyberfox 27    * @brief Contains all the methods to examine, control, and command a
cyberfox 28    * specific auction.
cyberfox 29    *
cyberfox 30    * Where the AuctionInfo class contains information which is purely
cyberfox 31    * retrieved from the server, the AuctionEntry class decorates that
cyberfox 32    * with things like when it was last updated, whether to snipe, any
cyberfox 33    * comment the user might have made on it, etc.
cyberfox 34    *
cyberfox 35    * I.e. AuctionEntry keeps track of things that the PROGRAM needs to
cyberfox 36    * know about the auction, not things that are inherent to auctions.
cyberfox 37    *
cyberfox 38    * This is not descended from AuctionInfo because the actual type of
cyberfox 39    * AuctionInfo varies per server.
mrs      40    *
cyberfox 41    * @author Morgan Schweers
cyberfox 42    * @see AuctionInfo
cyberfox 43    * @see SpecificAuction
cyberfox 44    */
mrs      45   public class AuctionEntry extends ActiveRecord implements Comparable<AuctionEntry>, EntryInterface {
mrs      46     private Category mCategory;
mrs      47     private static Resolver sResolver = null;
cyberfox 48   
cyberfox 49     /**
cyberfox 50      * @brief Set a status message, and mark that the connection is currently invalid.
cyberfox 51      */
cyberfox 52     public void logError() {
cyberfox 53       setLastStatus("Communications failure talking to the server.");
cyberfox 54       setInvalid();
cyberfox 55     }
cyberfox 56   
mrs      57     public Currency bestValue() {
mrs      58       if (isSniped()) {
mrs      59         return getSnipe().getAmount();
mrs      60       }
mrs      61   
mrs      62       return isBidOn() && !isComplete() ? getBid() : getCurBid();
mrs      63     }
mrs      64   
mrs      65     public Currency getSnipeAmount() {
mrs      66       return isSniped() ? getSnipe().getAmount() : Currency.NoValue();
mrs      67     }
mrs      68   
mrs      69     public int getSnipeQuantity() {
mrs      70       return isSniped() ? getSnipe().getQuantity() : 0;
mrs      71     }
mrs      72   
mrs      73     private AuctionSnipe getSnipe() {
mrs      74       if(mSnipe == null) {
mrs      75         if(get("snipe_id") != null) {
mrs      76           mSnipe = AuctionSnipe.find(get("snipe_id"));
mrs      77           if(mSnipe == null) {
mrs      78             //  Couldn't find the snipe in the database.
mrs      79             setInteger("snipe_id", null);
mrs      80             saveDB();
mrs      81           }
mrs      82         }
mrs      83       }
mrs      84       return mSnipe;
mrs      85     }
mrs      86   
cyberfox 87     /** All the auction-independant information like high bidder's name,
cyberfox 88      * seller's name, etc...  This is directly queried when this object
cyberfox 89      * is queried about any of those fields.
mrs      90      *
cyberfox 91      */
mrs      92     private AuctionInfo mAuction = null;
cyberfox 93   
cyberfox 94     /**
cyberfox 95      * A logging class for keeping track of events.
cyberfox 96      *
mrs      97      * @see com.jbidwatcher.auction.event.EventLogger
cyberfox 98      */
mrs      99     private EventLogger mEntryEvents = null;
cyberfox 100  
cyberfox 101    /**
cyberfox 102     * Are we in the middle of updating?  This should probably be
cyberfox 103     * synchronized, and therefore a Boolean.  BUGBUG -- mrs: 01-January-2003 23:59
cyberfox 104     */
mrs      105    private boolean mUpdating =false;
cyberfox 106  
cyberfox 107    /**
cyberfox 108     * Is it time to update this AuctionEntry?  This is used for things
cyberfox 109     * like sniping, where we want an immediate update afterwards.
cyberfox 110     */
mrs      111    private boolean mNeedsUpdate =false;
cyberfox 112  
cyberfox 113    /**
cyberfox 114     * Force an update despite ended status, for the post-end update,
cyberfox 115     * and for user-initiated updates of ended auctions.
cyberfox 116     */
mrs      117    private boolean mForceUpdate =false;
cyberfox 118  
cyberfox 119    /**
cyberfox 120     * Have we ever obtained this auction data from the server?
cyberfox 121     */
mrs      122    private boolean mLoaded =false;
cyberfox 123  
cyberfox 124    /**
cyberfox 125     * If this auction is part of a multiple-snipe, this value will not
cyberfox 126     * be null, and will point to a MultiSnipe object.
cyberfox 127     */
mrs      128    private MultiSnipe mMultiSnipe =null;
cyberfox 129  
cyberfox 130    /**
mrs      131     * Is the current user the seller?  Same caveats as mHighBidder.
cyberfox 132     */
mrs      133    private boolean mSeller =false;
cyberfox 134  
mrs      135    private AuctionSnipe mSnipe = null;
cyberfox 136  
cyberfox 137    /**
cyberfox 138     * How much was a cancelled snipe for?  (Recordkeeping)
cyberfox 139     */
mrs      140    private Currency mCancelSnipeBid = null;
cyberfox 141  
cyberfox 142    /**
cyberfox 143     * What AuctionServer is responsible for handling this
cyberfox 144     * AuctionEntry's actions?
cyberfox 145     */
mrs      146    private AuctionServerInterface mServer =null;
cyberfox 147  
cyberfox 148    /**
cyberfox 149     * The last time this auction was bid on.  Not presently used,
cyberfox 150     * although set, saved, and loaded consistently.
cyberfox 151     */
mrs      152    private long mBidAt =0;
cyberfox 153  
cyberfox 154    /**
cyberfox 155     * The last time this auction was updated from the server.
cyberfox 156     */
mrs      157    private long mLastUpdatedAt =0;
cyberfox 158  
cyberfox 159    /**
mrs      160     * Starting mQuickerUpdateStart milliseconds from the end of the
cyberfox 161     * auction, it will start triggering an update of the auction from
cyberfox 162     * the server once every minute.  Currently set so that at half an
cyberfox 163     * hour from the end of the auction, start updating every minute.
cyberfox 164     */
mrs      165    private long mQuickerUpdateStart = Constants.THIRTY_MINUTES;
cyberfox 166  
cyberfox 167    /**
mrs      168     * Every mUpdateFrequency milliseconds it will trigger an update of
cyberfox 169     * the auction from the server.
cyberfox 170     */
mrs      171    private long mUpdateFrequency = Constants.FORTY_MINUTES;
cyberfox 172  
cyberfox 173    /**
cyberfox 174     * Delta in time from the end of the auction that sniping will
cyberfox 175     * occur at.  It's possible to set a different snipe time for each
cyberfox 176     * auction, although it's not presently implemented through any UI.
cyberfox 177     */
mrs      178    private long mSnipeAt = -1;
cyberfox 179  
cyberfox 180    /**
cyberfox 181     * Default delta in time from the end of the auction that sniping
cyberfox 182     * will occur at.  This valus can be read and modified by
cyberfox 183     * getDefaultSnipeTime() & setDefaultSnipeTime().
cyberfox 184     */
mrs      185    private static long sDefaultSnipeAt = Constants.THIRTY_SECONDS;
cyberfox 186  
cyberfox 187    /**
cyberfox 188     * The time at which this will cease being a 'recently added'
cyberfox 189     * auction.  Usually set to five minutes after the construction.
cyberfox 190     */
mrs      191    private long mAddedRecently = 0;
cyberfox 192  
cyberfox 193    /**
cyberfox 194     * The time at which this wll cease being paused for update.  This
cyberfox 195     * allows the 'Stop' button to work properly.
cyberfox 196     */
mrs      197    private long mDontUpdate = 0;
cyberfox 198  
mrs      199    private StringBuffer mLastErrorPage = null;
cyberfox 200  
cyberfox 201    /**
cyberfox 202     * Does all the jobs of the constructors, so that the constructors
cyberfox 203     * become simple calls to this function.  Presets up all the
cyberfox 204     * necessary variables, loads any data in, sets the lastUpdated
cyberfox 205     * flag, all the timers, retrieves the auction if necessary.
mrs      206     *
cyberfox 207     * @param auctionIdentifier - Each auction site has an identifier that
cyberfox 208     *                            is used to key the auction.
cyberfox 209     */
cyberfox 210    private synchronized void prepareAuctionEntry(String auctionIdentifier) {
mrs      211      mLastUpdatedAt = 0;
mrs      212      mNeedsUpdate = true;
cyberfox 213  
mrs      214      if (mServer != null) {
mrs      215        mAuction = mServer.create(auctionIdentifier);
cyberfox 216      }
cyberfox 217  
mrs      218      mLoaded = mAuction != null;
mrs      219  
cyberfox 220      /**
cyberfox 221       * Note that a bad auction (couldn't get an auction server, or a
cyberfox 222       * specific auction info object) doesn't have an identifier, and
mrs      223       * isn't loaded.  This will fail out the init process, and this
mrs      224       * will never be added to the items list.
cyberfox 225       */
mrs      226      if (mLoaded) {
mrs      227        if(mAuction.getServer() != null) setServer((AuctionServerInterface)mAuction.getServer());
mrs      228        setDefaultCurrency(mAuction.getCurBid());
mrs      229        updateHighBid();
mrs      230        checkHighBidder();
cyberfox 231        checkSeller();
cyberfox 232        checkEnded();
cyberfox 233      }
cyberfox 234    }
cyberfox 235  
cyberfox 236    ///////////////
cyberfox 237    //  Constructor
cyberfox 238  
cyberfox 239    /** Construct an AuctionEntry from just the ID, loading all necessary info
cyberfox 240     * from the server.
mrs      241     *
mrs      242     * @param auctionIdentifier The auction ID, from which the entire
cyberfox 243     *     AuctionEntry is built by loading data from the server.
mrs      244     * @param server - The auction server for this entry.
cyberfox 245     */
mrs      246    private AuctionEntry(String auctionIdentifier, AuctionServerInterface server) {
mrs      247      mServer = server;
cyberfox 248      checkConfigurationSnipeTime();
mrs      249      mAddedRecently = System.currentTimeMillis() + 5 * Constants.ONE_MINUTE;
cyberfox 250      prepareAuctionEntry(auctionIdentifier);
cyberfox 251    }
cyberfox 252  
mrs      253    /**
mrs      254     * Create a new auction entry for the ID passed in.  If it is in the deleted list, or already exists in
mrs      255     * the database, it will return null.
mrs      256     *
mrs      257     * @param identifier - The auction identifier to create an auction for.
mrs      258     *
mrs      259     * @return - null if the auction is in the deleted entry table, or the existing auction
mrs      260     * entry table, otherwise returns a valid AuctionEntry for the auction identifier provided.
mrs      261     */
mrs      262    public static AuctionEntry construct(String identifier) {
mrs      263      AuctionServerInterface server = sResolver.getServer();
mrs      264      String strippedId = server.stripId(identifier);
mrs      265  
mrs      266      if (!DeletedEntry.exists(strippedId) && findByIdentifier(strippedId) == null) {
mrs      267        AuctionEntry ae = new AuctionEntry(strippedId, server);
mrs      268        if(ae.isLoaded()) {
mrs      269          String id = ae.saveDB();
mrs      270          if (id != null) {
mrs      271            JConfig.increment("stats.auctions");
mrs      272            return ae;
mrs      273          }
mrs      274        }
mrs      275      }
mrs      276      return null;
mrs      277    }
mrs      278  
cyberfox 279    /**
mrs      280     * A constructor that does almost nothing.  This is to be used
cyberfox 281     * for loading from XML data later on, where the fromXML function
mrs      282     * will fill out all the internal information.  Similarly, ActiveRecord
mrs      283     * fills this out when pulling from a database record.
mrs      284     *
mrs      285     * Uses the default server.
cyberfox 286     */
cyberfox 287    public AuctionEntry() {
mrs      288      mServer = sResolver.getServer();
cyberfox 289      checkConfigurationSnipeTime();
cyberfox 290    }
cyberfox 291  
cyberfox 292    /**
cyberfox 293     * @brief Look up to see if the auction is ended yet, just sets
mrs      294     * mComplete if it is.
cyberfox 295     */
cyberfox 296    private void checkEnded() {
mrs      297      if(!isComplete()) {
cyberfox 298        Date serverTime = new Date(System.currentTimeMillis() +
mrs      299                                   getServer().getServerTimeDelta());
cyberfox 300  
cyberfox 301        //  If we're past the end time, update once, and never again.
cyberfox 302        if(serverTime.after(getEndDate())) {
mrs      303          setComplete(true);
cyberfox 304        }
cyberfox 305      }
cyberfox 306    }
cyberfox 307  
cyberfox 308    /////////////
cyberfox 309    //  Accessors
cyberfox 310  
cyberfox 311    /**
cyberfox 312     * @brief Return the server associated with this entry.
mrs      313     *
cyberfox 314     * @return The server that this auction entry is associated with.
cyberfox 315     */
mrs      316    public AuctionServerInterface getServer() {
mrs      317      if(mServer == null) {
mrs      318        mServer = sResolver.getServer();
mrs      319      }
mrs      320      return(mServer);
mrs      321    }
mrs      322  
cyberfox 323    /**
cyberfox 324     * @brief Set the auction server for this entry.
cyberfox 325     *
mrs      326     * First, if there are any snipes in the 'old' server, cancel them.
mrs      327     * Then set the server to the passed in value.
mrs      328     * Then re-set up any snipes associated with the listing.
mrs      329     *
cyberfox 330     * @param newServer - The server to associate with this auction entry.
cyberfox 331     */
mrs      332    public void setServer(AuctionServerInterface newServer) {
mrs      333      if(newServer != mServer) {
mrs      334        //  "CANCEL_SNIPE #{id}"
mrs      335        if(isSniped()) getServer().cancelSnipe(getIdentifier());
mrs      336        mServer = newServer;
mrs      337        if(isSniped()) getServer().setSnipe(this);
mrs      338      }
mrs      339    }
cyberfox 340  
cyberfox 341    /**
cyberfox 342     * @brief Query whether this entry has ever been loaded from the server.
mrs      343     *
cyberfox 344     * Really shouldn't be necessary, but is.  If we try to create an
cyberfox 345     * AuctionEntry with a bad identifier, that doesn't match any
cyberfox 346     * server, or isn't 'live' on the auction server, we need an error
cyberfox 347     * of this sort, to identify that the load failed.  This is mainly
cyberfox 348     * because constructors don't fail.
cyberfox 349     *
cyberfox 350     * @return Whether this entry has ever been loaded from the server.
cyberfox 351     */
mrs      352    public boolean isLoaded()    { return(mLoaded); }
cyberfox 353  
cyberfox 354    /**
cyberfox 355     * @brief Check if the current snipe value would be a valid bid currently.
cyberfox 356     *
cyberfox 357     * @return true if the current snipe is at least one minimum bid
cyberfox 358     * increment over the current high bid.  Returns false otherwise.
cyberfox 359     */
cyberfox 360    public boolean isSnipeValid() {
mrs      361      if(getSnipe() == null) return false;
mrs      362  
mrs      363      Currency minIncrement = getServer().getMinimumBidIncrement(getCurBid(), getNumBidders());
cyberfox 364      Currency nextBid = Currency.NoValue();
cyberfox 365      boolean rval = false;
cyberfox 366  
cyberfox 367      try {
cyberfox 368        nextBid = getCurBid().add(minIncrement);
cyberfox 369  
mrs      370        if(nextBid == null || getSnipe().getAmount().getValue() >= nextBid.getValue()) {
cyberfox 371          rval = true;
cyberfox 372        }
cyberfox 373      } catch(Currency.CurrencyTypeException cte) {
mrs      374        JConfig.log().handleException("This should never happen (" + nextBid + ", " + getSnipe().getAmount() + ")!", cte);
cyberfox 375      }
cyberfox 376  
cyberfox 377      return rval;
cyberfox 378    }
cyberfox 379  
cyberfox 380    /**
cyberfox 381     * @brief Check if the user has an outstanding snipe on this auction.
mrs      382     *
cyberfox 383     * @return Whether there is a snipe waiting on this auction.
cyberfox 384     */
mrs      385    public boolean isSniped() {
mrs      386      return getSnipe() != null;
mrs      387    }
cyberfox 388  
cyberfox 389    /**
cyberfox 390     * @brief Check if this auction is part of a snipe group.
cyberfox 391     *
cyberfox 392     * Multisnipes are snipes where each fires, and if one is successful
cyberfox 393     * then it automatically cancels all the rest of the snipes.  This
cyberfox 394     * lets users snipe on (say) five auctions, even though they only
cyberfox 395     * want one of the items.
mrs      396     *
cyberfox 397     * @return Whether this auction is one of a multisnipe group, where
cyberfox 398     * each auction is sniped on until one is won.
cyberfox 399     */
mrs      400    public boolean isMultiSniped()    { return(getMultiSnipe() != null); }
cyberfox 401  
cyberfox 402    /**
cyberfox 403     * @brief Check if the user has ever placed a bid (or completed
cyberfox 404     * snipe) on this auction.
mrs      405     *
cyberfox 406     * @return Whether the user has ever actually submitted a bid to the
cyberfox 407     * server for this auction.
cyberfox 408     */
mrs      409    public boolean isBidOn() { return(getBid() != null && !getBid().isNull()); }
cyberfox 410  
cyberfox 411    /**
cyberfox 412     * @brief Check if we are in the midst of updating this auction.
mrs      413     *
cyberfox 414     * Not necessary, as the only place it should be used is internally,
cyberfox 415     * but it's now being used by auctionTableModel to identify when a
cyberfox 416     * specific item is being updated.  It lets the item # be a nice red,
cyberfox 417     * momentarily, while the update happens.
cyberfox 418     *
cyberfox 419     * @return Whether the update for this auction is in progress.
cyberfox 420     */
mrs      421    public boolean isUpdating()  { return(mUpdating); }
cyberfox 422  
cyberfox 423    /**
cyberfox 424     * @brief Check if the current user is the high bidder on this
cyberfox 425     * auction.
cyberfox 426     *
cyberfox 427     * This should eventually handle multiple users per server, so that
cyberfox 428     * users can have multiple identities per auction site.
mrs      429     *
cyberfox 430     * @return Whether the current user is the high bidder.
cyberfox 431     */
mrs      432    public boolean isHighBidder() { return isWinning(); }
cyberfox 433  
mrs      434    public boolean isWinning() { return getBoolean("winning", false); }
mrs      435    public void setWinning(boolean state) { setBoolean("winning", state); }
mrs      436  
cyberfox 437    /**
cyberfox 438     * @brief Check if the current user is the seller for this auction.
cyberfox 439     *
cyberfox 440     * This should eventually handle multiple users per server, so that
cyberfox 441     * users can have multiple identities per auction site.
cyberfox 442     * FUTURE FEATURE -- mrs: 02-January-2003 01:25
mrs      443     *
cyberfox 444     * @return Whether the current user is the seller.
cyberfox 445     */
mrs      446    public boolean isSeller() { return mSeller; }
cyberfox 447  
cyberfox 448    /**
cyberfox 449     * @brief What was the highest amount actually submitted to the
cyberfox 450     * server as a bid?
mrs      451     *
cyberfox 452     * With some auction servers, it might be possible to find out how
cyberfox 453     * much the user bid, but in general presume this value is only set
cyberfox 454     * by bidding through this program, or firing a snipe.
cyberfox 455     *
cyberfox 456     * @return The highest amount bid through this program.
cyberfox 457     */
mrs      458    public Currency getBid()  { return getMonetary("last_bid_amount"); }
cyberfox 459  
cyberfox 460    /**
cyberfox 461     * @brief Set the highest amount actually submitted to the server as a bid.
mrs      462     * What is the maximum amount the user bid on the last time they bid?
mrs      463     *
cyberfox 464     * @param highBid - The new high bid value to set for this auction.
cyberfox 465     */
cyberfox 466    public void setBid(Currency highBid)  {
mrs      467      setMonetary("last_bid_amount", highBid == null? Currency.NoValue() : highBid);
mrs      468      saveDB();
cyberfox 469    }
cyberfox 470  
mrs      471    public void setBidQuantity(int quant) {
mrs      472      setInteger("last_bid_quantity", quant);
mrs      473      saveDB();
mrs      474    }
cyberfox 475  
cyberfox 476    /**
cyberfox 477     * @brief What was the most recent number of items actually
cyberfox 478     * submitted to the server as part of a bid?
mrs      479     * How many items were bid on the last time the user bid?
mrs      480     *
cyberfox 481     * @return The count of items bid on the last time a user bid.
cyberfox 482     */
mrs      483    public int getBidQuantity() {
mrs      484      if(isBidOn()) {
mrs      485        Integer i = getInteger("last_bid_quantity");
mrs      486        return i != null ? i : 1;
mrs      487      }
mrs      488      return 0;
mrs      489    }
cyberfox 490  
cyberfox 491    /**
cyberfox 492     * @brief Set this auction as being part of a multi-snipe set,
cyberfox 493     * change the multi-snipe group associated with it, or delete it
cyberfox 494     * from it's current multi-snipe set.
cyberfox 495     *
cyberfox 496     * TODO -- Extract this out, create a SnipeInterface which would be
cyberfox 497     * implemented by AuctionEntry.  Multisnipe then operates on
cyberfox 498     * SnipeInterface objects, so we don't have the X calls Y, Y calls
cyberfox 499     * X interrelationship.
cyberfox 500     *
cyberfox 501     * @param inMS - The multisnipe to set or change.  If it's 'null',
cyberfox 502     * it clears the multisnipe for this entry.
cyberfox 503     */
cyberfox 504    public void setMultiSnipe(MultiSnipe inMS) {
cyberfox 505      //  Shortcut: if no change, leave.
mrs      506      if(mMultiSnipe != inMS) {
cyberfox 507        //  If there was a different MultiSnipe before, remove this from it.
mrs      508        if(mMultiSnipe != null) {
mrs      509          mMultiSnipe.remove(getIdentifier());
mrs      510          //  ...and cancel the current snipe, as long as we're not
mrs      511          // cancelling this snipe entirely (in which case we cancel
mrs      512          // it below).
mrs      513          if(inMS != null) {
mrs      514            prepareSnipe(Currency.NoValue(), 0);
mrs      515          } else {
mrs      516            setInteger("multisnipe_id", null);
mrs      517          }
cyberfox 518        }
mrs      519        mMultiSnipe = inMS;
cyberfox 520        //  If we weren't just deleting, then prepare the new snipe, and
cyberfox 521        //  add to the multi-snipe group.
mrs      522        if(mMultiSnipe != null) {
cyberfox 523          if(!isSniped()) {
mrs      524            prepareSnipe(mMultiSnipe.getSnipeValue(getShippingWithInsurance()));
cyberfox 525          }
mrs      526          mMultiSnipe.add(getIdentifier());
mrs      527          addMulti(mMultiSnipe);
cyberfox 528        }
cyberfox 529      }
cyberfox 530  
cyberfox 531      if(inMS == null) {
cyberfox 532        //  If the multisnipe was null, remove the snipe entirely.
cyberfox 533        prepareSnipe(Currency.NoValue(), 0);
mrs      534        setInteger("multisnipe_id", null);
mrs      535      } else {
mrs      536        setInteger("multisnipe_id", inMS.getId());
cyberfox 537      }
mrs      538      saveDB();
cyberfox 539    }
cyberfox 540  
cyberfox 541    /**
cyberfox 542     * @brief Get the default snipe time as configured.
mrs      543     *
cyberfox 544     * @return - The default snipe time from the configuration.  If it's
cyberfox 545     * not set, return a standard 30 seconds.
cyberfox 546     */
cyberfox 547    private static long getGlobalSnipeTime() {
cyberfox 548      long snipeTime;
cyberfox 549  
mrs      550      String strConfigSnipeAt = JConfig.queryConfiguration("snipemilliseconds");
cyberfox 551      if(strConfigSnipeAt != null) {
cyberfox 552        snipeTime = Long.parseLong(strConfigSnipeAt);
cyberfox 553      } else {
mrs      554        snipeTime = Constants.THIRTY_SECONDS;
cyberfox 555      }
cyberfox 556  
cyberfox 557      return snipeTime;
cyberfox 558    }
cyberfox 559  
cyberfox 560    /**
cyberfox 561     * @brief Get the multi-snipe object associated with this auction, if it's set as a multi-snipe.
cyberfox 562     *
cyberfox 563     * @return - A multisnipe object or null if there isn't any multisnipe set.
cyberfox 564     */
mrs      565    public MultiSnipe getMultiSnipe() {
mrs      566      if(mMultiSnipe != null) return mMultiSnipe;
cyberfox 567  
mrs      568      Integer id = getInteger("multisnipe_id");
mrs      569      if(id == null) return null;
mrs      570  
mrs      571      mMultiSnipe = MultiSnipe.find(id);
mrs      572      return mMultiSnipe;
mrs      573    }
mrs      574  
cyberfox 575    /**
cyberfox 576     * @brief Check if the configuration has a 'snipemilliseconds'
cyberfox 577     * entry, and update the default if it does.
cyberfox 578     */
cyberfox 579    private void checkConfigurationSnipeTime() {
mrs      580      sDefaultSnipeAt = getGlobalSnipeTime();
cyberfox 581    }
cyberfox 582  
cyberfox 583    /**
cyberfox 584     * @brief Set how long before auctions are complete to fire snipes
cyberfox 585     * for any auction using the default snipe timer.
cyberfox 586     *
cyberfox 587     * @param newSnipeAt - The number of milliseconds prior to the end
cyberfox 588     * of auctions that the snipe timer will fire.  Can be overridden by
cyberfox 589     * setSnipeTime() on a per-auction basis.
cyberfox 590     */
cyberfox 591    public static void setDefaultSnipeTime(long newSnipeAt) {
mrs      592      sDefaultSnipeAt = newSnipeAt;
cyberfox 593    }
cyberfox 594  
cyberfox 595    public long getSnipeTime() {
mrs      596      return hasDefaultSnipeTime()? sDefaultSnipeAt : mSnipeAt;
cyberfox 597    }
cyberfox 598  
cyberfox 599    public boolean hasDefaultSnipeTime() {
mrs      600      return(mSnipeAt == -1);
cyberfox 601    }
cyberfox 602  
cyberfox 603    public void setSnipeTime(long newSnipeTime) {
mrs      604      mSnipeAt = newSnipeTime;
cyberfox 605    }
cyberfox 606  
cyberfox 607    /**
cyberfox 608     * @brief Get the time when this entry will no longer be considered
cyberfox 609     * 'newly added', or null if it's been cleared, or is already past.
mrs      610     *
cyberfox 611     * @return The time at which this entry is no longer new.
cyberfox 612     */
cyberfox 613    public long getJustAdded() {
mrs      614      return mAddedRecently;
cyberfox 615    }
cyberfox 616  
mrs      617    public String getIdentifier() {
mrs      618      return getAuction() == null ? null : getAuction().getIdentifier();
mrs      619    }
cyberfox 620  
cyberfox 621    ///////////////////////////
cyberfox 622    //  Actual logic functions
cyberfox 623  
mrs      624    public void updateHighBid() {
mrs      625      int numBidders = getNumBidders();
mrs      626  
mrs      627      if (numBidders > 0 || isFixed()) {
mrs      628        getServer().updateHighBid(this);
mrs      629      }
mrs      630    }
mrs      631  
cyberfox 632    /**
cyberfox 633     * @brief On update, we check if we're the high bidder.
cyberfox 634     *
cyberfox 635     * When you change user ID's, you should force a complete update, so
cyberfox 636     * this is synchronized correctly.
cyberfox 637     */
mrs      638    private void checkHighBidder() {
cyberfox 639      int numBidders = getNumBidders();
cyberfox 640  
cyberfox 641      if(numBidders > 0) {
cyberfox 642        if(isBidOn() && isPrivate()) {
cyberfox 643          Currency curBid = getCurBid();
cyberfox 644          try {
mrs      645            if(curBid.less(getBid())) setWinning(true);
cyberfox 646          } catch(Currency.CurrencyTypeException cte) {
cyberfox 647            /* Should never happen...?  */
mrs      648            JConfig.log().handleException("This should never happen (bad Currency at this point!).", cte);
cyberfox 649          }
mrs      650          if(curBid.equals(getBid())) {
mrs      651            setWinning(numBidders == 1);
mrs      652            //  winning == false means that there are multiple bidders, and the price that
cyberfox 653            //  two (this user, and one other) bid are exactly the same.  How
cyberfox 654            //  do we know who's first, given that it's a private auction?
cyberfox 655            //
cyberfox 656            //  The only answer I have is to presume that we're NOT first.
cyberfox 657            //  eBay knows the 'true' answer, but how to extract it from them...
cyberfox 658          }
cyberfox 659        } else {
mrs      660          setWinning(getServer().isCurrentUser(getHighBidder()));
cyberfox 661        }
cyberfox 662      }
cyberfox 663    }
cyberfox 664  
cyberfox 665    /**
cyberfox 666     * @brief Set the flags if the current user is the seller in this auction.
cyberfox 667     */
cyberfox 668    private void checkSeller() {
mrs      669      mSeller = getServer().isCurrentUser(getSeller());
cyberfox 670    }
cyberfox 671  
cyberfox 672    ////////////////////////////
cyberfox 673    //  Periodic logic functions
cyberfox 674  
cyberfox 675    /**
cyberfox 676     * @brief Determine if it's time to update this auction.
cyberfox 677     *
cyberfox 678     * PMD bitches long and hard about assigning to null repeatedly in
cyberfox 679     * this function.  Any way to clean that up? -- mrs: 23-February-2003 22:28
mrs      680     *
cyberfox 681     * @return Whether or not it's time to retrieve the updated state of
cyberfox 682     * this auction.
cyberfox 683     */
cyberfox 684    public synchronized boolean checkUpdate() {
cyberfox 685      long curTime = System.currentTimeMillis();
mrs      686      if(mAddedRecently != 0) {
mrs      687        if(curTime > mAddedRecently) mAddedRecently = 0;
cyberfox 688      }
cyberfox 689  
mrs      690      if(mDontUpdate != 0) {
mrs      691        if(curTime > mDontUpdate) {
mrs      692          mDontUpdate = 0;
cyberfox 693        } else {
cyberfox 694          return false;
cyberfox 695        }
cyberfox 696      }
cyberfox 697  
mrs      698      if(!mNeedsUpdate) {
mrs      699        if(!isUpdating() && !isComplete()) {
mrs      700          long serverTime = curTime + getServer().getServerTimeDelta();
cyberfox 701  
cyberfox 702          //  If we're past the end time, update once, and never again.
cyberfox 703          if(serverTime > getEndDate().getTime()) {
mrs      704            mNeedsUpdate = true;
cyberfox 705          } else {
mrs      706            if( mUpdateFrequency != Constants.ONE_MINUTE ) {
mrs      707              if( (getEndDate().getTime() - mQuickerUpdateStart) < serverTime) {
mrs      708                mUpdateFrequency = Constants.ONE_MINUTE;
mrs      709                mNeedsUpdate = true;
cyberfox 710              }
cyberfox 711            }
mrs      712            if( (mLastUpdatedAt + mUpdateFrequency) < curTime) {
mrs      713              mNeedsUpdate = true;
cyberfox 714            }
cyberfox 715          }
cyberfox 716        }
cyberfox 717      }
cyberfox 718  
mrs      719      return mNeedsUpdate;
cyberfox 720    }
cyberfox 721  
cyberfox 722    /**
cyberfox 723     * @brief Get the next update time.
cyberfox 724     *
cyberfox 725     * @return The last time it was updated, plus the update frequency.
cyberfox 726     */
mrs      727    public long getNextUpdate() { return ((mLastUpdatedAt ==0)?System.currentTimeMillis(): mLastUpdatedAt) + mUpdateFrequency; }
cyberfox 728  
cyberfox 729    /**
cyberfox 730     * @brief Mark this entry as being not-invalid.
cyberfox 731     */
cyberfox 732    public void clearInvalid() {
mrs      733      setBoolean("invalid", false);
mrs      734      saveDB();
cyberfox 735    }
cyberfox 736  
cyberfox 737    /**
cyberfox 738     * @brief Mark this entry as being invalid for some reason.
cyberfox 739     */
cyberfox 740    public void setInvalid() {
mrs      741      setBoolean("invalid", true);
mrs      742      saveDB();
cyberfox 743    }
cyberfox 744  
cyberfox 745    /**
cyberfox 746     * @brief Is this entry invalid for any reason?
cyberfox 747     *
mrs      748     * Is the data reasonably synchronized with the server?  (When the
mrs      749     * site stops providing the data, or an error occurs when retrieving
mrs      750     * this auction, this will be true.)
mrs      751     *
cyberfox 752     * @return - True if this auction is considered invalid, false if it's okay.
cyberfox 753     */
cyberfox 754    public boolean isInvalid() {
mrs      755      return getBoolean("invalid", false);
cyberfox 756    }
cyberfox 757  
cyberfox 758    /**
cyberfox 759     * @brief Store a user-specified comment about this item.
mrs      760     * Allow the user to add a personal comment about this auction.
mrs      761     *
cyberfox 762     * @param newComment - The comment to keep track of.  If it's empty,
cyberfox 763     * we effectively delete the comment.
cyberfox 764     */
cyberfox 765    public void setComment(String newComment) {
mrs      766      if(newComment.trim().length() == 0)
mrs      767        setString("comment", null);
cyberfox 768      else
mrs      769        setString("comment", newComment.trim());
mrs      770      saveDB();
cyberfox 771    }
cyberfox 772  
cyberfox 773    /**
cyberfox 774     * @brief Get any user-specified comment regarding this auction.
mrs      775     *
cyberfox 776     * @return Any comment the user may have stored about this item.
cyberfox 777     */
cyberfox 778    public String getComment() {
mrs      779      return getString("comment");
cyberfox 780    }
cyberfox 781  
cyberfox 782    /**
cyberfox 783     * @brief Add an auction-specific status message into its own event log.
mrs      784     *
cyberfox 785     * @param inStatus - A string that explains what the event is.
cyberfox 786     */
cyberfox 787    public void setLastStatus(String inStatus) {
mrs      788      getEvents().setLastStatus(inStatus);
cyberfox 789    }
cyberfox 790  
cyberfox 791    public void setShipping(Currency newShipping) {
mrs      792      setMonetary("shipping", newShipping);
mrs      793      saveDB();
cyberfox 794    }
cyberfox 795  
cyberfox 796    /**
cyberfox 797     * @brief Get a plain version of the event list, where each line is
cyberfox 798     * a seperate event, including the title and identifier.
mrs      799     *
cyberfox 800     * @return A string with all the event information included.
cyberfox 801     */
mrs      802    public String getLastStatus() { return getEvents().getLastStatus(); }
mrs      803  
cyberfox 804    /**
cyberfox 805     * @brief Get either a plain version of the events, or a complex
cyberfox 806     * (bulk) version which doesn't include the title and identifier,
cyberfox 807     * since those are set by the AuctionEntry itself, and are based
cyberfox 808     * on its own data.
mrs      809     *
cyberfox 810     * @return A string with all the event information included.
cyberfox 811     */
mrs      812    public String getStatusHistory() {
mrs      813      return getEvents().getAllStatuses();
cyberfox 814    }
cyberfox 815  
cyberfox 816    public int getStatusCount() {
mrs      817      return getEvents().getStatusCount();
cyberfox 818    }
mrs      819  
mrs      820    private EventLogger getEvents() {
mrs      821      if(mEntryEvents == null) mEntryEvents = new EventLogger(getIdentifier(), getId(), getTitle());
mrs      822      return mEntryEvents;
mrs      823    }
mrs      824  
cyberfox 825    //////////////////////////
cyberfox 826    //  XML Handling functions
cyberfox 827  
mrs      828    protected String[] infoTags = { "info", "bid", "snipe", "complete", "invalid", "comment", "log", "multisnipe", "shipping", "category", "winning" };
cyberfox 829    protected String[] getTags() { return infoTags; }
cyberfox 830  
cyberfox 831    /**
cyberfox 832     * @brief XML load-handling.  It would be really nice to be able to
cyberfox 833     * abstract this for all the classes that serialize to XML.
mrs      834     *
cyberfox 835     * @param tagId - The index into 'entryTags' for the current tag.
cyberfox 836     * @param curElement - The current XML element that we're loading from.
cyberfox 837     */
cyberfox 838    protected void handleTag(int tagId, XMLElement curElement) {
cyberfox 839      switch(tagId) {
cyberfox 840        case 0:  //  Get the general auction information
mrs      841          //  TODO -- What if it's already in the database?
mrs      842          mAuction.fromXML(curElement);
mrs      843          mAuction.saveDB();
cyberfox 844          break;
cyberfox 845        case 1:  //  Get bid info
cyberfox 846          Currency bidAmount = Currency.getCurrency(curElement.getProperty("CURRENCY"),
cyberfox 847                                            curElement.getProperty("PRICE"));
cyberfox 848          setBid(bidAmount);
mrs      849          setBidQuantity(Integer.parseInt(curElement.getProperty("QUANTITY")));
cyberfox 850          if(curElement.getProperty("WHEN", null) != null) {
mrs      851            mBidAt = Long.parseLong(curElement.getProperty("WHEN"));
cyberfox 852          }
cyberfox 853          break;
cyberfox 854        case 2:  //  Get the snipe info together
cyberfox 855          Currency snipeAmount = Currency.getCurrency(curElement.getProperty("CURRENCY"),
cyberfox 856                                              curElement.getProperty("PRICE"));
cyberfox 857          prepareSnipe(snipeAmount, Integer.parseInt(curElement.getProperty("QUANTITY")));
mrs      858          mSnipeAt = Long.parseLong(curElement.getProperty("SECONDSPRIOR"));
cyberfox 859          break;
cyberfox 860        case 3:
mrs      861          setComplete(true);
cyberfox 862          break;
cyberfox 863        case 4:
mrs      864          setInvalid();
cyberfox 865          break;
cyberfox 866        case 5:
mrs      867          setComment(curElement.getContents());
cyberfox 868          break;
cyberfox 869        case 6:
mrs      870          mEntryEvents = new EventLogger(getIdentifier(), getId(), getTitle());
mrs      871          mEntryEvents.fromXML(curElement);
cyberfox 872          break;
cyberfox 873        case 7:
mrs      874          setMultiSnipe(MultiSnipe.loadFromXML(curElement));
cyberfox 875          break;
cyberfox 876        case 8:
mrs      877          Currency shipping = Currency.getCurrency(curElement.getProperty("CURRENCY"),
cyberfox 878                                           curElement.getProperty("PRICE"));
mrs      879          setShipping(shipping);
cyberfox 880          break;
cyberfox 881        case 9:
mrs      882          setCategory(curElement.getContents());
mrs      883          setSticky(curElement.getProperty("STICKY", "false").equals("true"));
cyberfox 884          break;
mrs      885        case 10:
mrs      886          setWinning(true);
mrs      887          break;
cyberfox 888        default:
cyberfox 889          break;
cyberfox 890          // commented out for FORWARDS compatibility.
cyberfox 891          //        throw new RuntimeException("Unexpected value when handling AuctionEntry tags!");
cyberfox 892      }
cyberfox 893    }
cyberfox 894  
cyberfox 895    /**
cyberfox 896     * @brief Check everything and build an XML element that contains as
cyberfox 897     * children all of the values that need storing for this item.
cyberfox 898     *
cyberfox 899     * This would be so much more useful if it were 'standard'.
mrs      900     *
cyberfox 901     * @return An XMLElement containing as children, all of the key
cyberfox 902     * values associated with this auction entry.
cyberfox 903     */
cyberfox 904    public XMLElement toXML() {
cyberfox 905      XMLElement xmlResult = new XMLElement("auction");
cyberfox 906  
cyberfox 907      xmlResult.setProperty("id", getIdentifier());
mrs      908      xmlResult.addChild(getAuction().toXML());
cyberfox 909  
cyberfox 910      if(isBidOn()) {
mrs      911        XMLElement xbid = new XMLElement("bid");
cyberfox 912        xbid.setEmpty();
mrs      913        xbid.setProperty("quantity", Integer.toString(getBidQuantity()));
mrs      914        xbid.setProperty("currency", getBid().fullCurrencyName());
mrs      915        xbid.setProperty("price", Double.toString(getBid().getValue()));
mrs      916        if(mBidAt != 0) {
mrs      917          xbid.setProperty("when", Long.toString(mBidAt));
cyberfox 918        }
cyberfox 919        xmlResult.addChild(xbid);
cyberfox 920      }
cyberfox 921  
cyberfox 922      if(isSniped()) {
mrs      923        XMLElement xsnipe = new XMLElement("snipe");
cyberfox 924        xsnipe.setEmpty();
mrs      925        xsnipe.setProperty("quantity", Integer.toString(getSnipe().getQuantity()));
mrs      926        xsnipe.setProperty("currency", getSnipe().getAmount().fullCurrencyName());
mrs      927        xsnipe.setProperty("price", Double.toString(getSnipe().getAmount().getValue()));
mrs      928        xsnipe.setProperty("secondsprior", Long.toString(mSnipeAt));
cyberfox 929        xmlResult.addChild(xsnipe);
cyberfox 930      }
cyberfox 931  
mrs      932      if(isMultiSniped()) xmlResult.addChild(getMultiSnipe().toXML());
cyberfox 933  
mrs      934      if(isComplete()) addStatusXML(xmlResult, "complete");
mrs      935      if(isInvalid()) addStatusXML(xmlResult, "invalid");
mrs      936      if(isDeleted()) addStatusXML(xmlResult, "deleted");
mrs      937      if(isWinning()) addStatusXML(xmlResult, "winning");
cyberfox 938  
mrs      939      if(getComment() != null) {
mrs      940        XMLElement xcomment = new XMLElement("comment");
mrs      941        xcomment.setContents(getComment());
cyberfox 942        xmlResult.addChild(xcomment);
cyberfox 943      }
cyberfox 944  
mrs      945      if(getCategory() != null) {
mrs      946        XMLElement xcategory = new XMLElement("category");
mrs      947        xcategory.setContents(getCategory());
mrs      948        xcategory.setProperty("sticky", isSticky() ?"true":"false");
cyberfox 949        xmlResult.addChild(xcategory);
cyberfox 950      }
cyberfox 951  
mrs      952      if(getShipping() != null) {
mrs      953        XMLElement xshipping = new XMLElement("shipping");
cyberfox 954        xshipping.setEmpty();
mrs      955        xshipping.setProperty("currency", getShipping().fullCurrencyName());
mrs      956        xshipping.setProperty("price", Double.toString(getShipping().getValue()));
cyberfox 957        xmlResult.addChild(xshipping);
cyberfox 958      }
cyberfox 959  
mrs      960      if(mEntryEvents != null) {
mrs      961        XMLElement xlog = mEntryEvents.toXML();
mrs      962        if (xlog != null) {
mrs      963          xmlResult.addChild(xlog);
mrs      964        }
cyberfox 965      }
cyberfox 966      return xmlResult;
cyberfox 967    }
cyberfox 968  
cyberfox 969    /**
cyberfox 970     * @brief Load auction entries from an XML element.
mrs      971     *
cyberfox 972     * @param inXML - The XMLElement that contains the items to load.
cyberfox 973     */
mrs      974    public void fromXML(XMLInterface inXML) {
cyberfox 975      String inID = inXML.getProperty("ID", null);
cyberfox 976      if(inID != null) {
mrs      977        mAuction = new AuctionInfo();
mrs      978        mAuction.setIdentifier(inID);
cyberfox 979  
cyberfox 980        super.fromXML(inXML);
cyberfox 981  
mrs      982        mLoaded = false;
cyberfox 983  
mrs      984        mLastUpdatedAt = 0;
cyberfox 985  
mrs      986        if(!isComplete()) setNeedsUpdate();
cyberfox 987  
mrs      988        saveDB();
mrs      989        if(mEntryEvents == null) {
mrs      990          getEvents();
cyberfox 991        }
mrs      992        checkHighBidder();
cyberfox 993        checkSeller();
mrs      994        saveDB();
cyberfox 995      }
cyberfox 996    }
cyberfox 997  
cyberfox 998    ////////////////////////////////
cyberfox 999    //  Multisnipe utility functions
1000  
1001    private static Map<Long, MultiSnipe> allMultiSnipes = new TreeMap<Long, MultiSnipe>();
1002  
1003    /**
1004     * @brief Add a new multisnipe to the AuctionEntry class's list of
1005     * multisnipes.
1006     *
1007     * This keeps track of ALL multisnipes, so that they can be
1008     * loaded/saved okay, as well as checked to remove.
1009     *
1010     * @param newMS - The newly created multisnipe to add.
1011     */
1012    private void addMulti(MultiSnipe newMS) {
1013      long newId = newMS.getIdentifier();
1014  
1015      if(!allMultiSnipes.containsKey(newId)) {
1016        allMultiSnipes.put(newId, newMS);
1017      }
1018    }
1019  
1020    /////////////////////
1021    //  Sniping functions
1022  
1023    /**
1024     * @brief Return whether this entry ever had a snipe cancelled or not.
1025     *
1026     * @return - true if a snipe was cancelled, false otherwise.
1027     */
1028    public boolean snipeCancelled() { return mCancelSnipeBid != null; }
1029  
1030    /**
1031     * @brief Return the amount that the snipe bid was for, before it
1032     * was cancelled.
1033     *
1034     * @return - A currency amount that was set to snipe, but cancelled.
1035     */
1036    public Currency getCancelledSnipe() { return mCancelSnipeBid; }
1037  
1038    /**
1039     * Cancel the snipe and clear the multisnipe setting.  This is used for
1040     * user-driven snipe cancellations, and errors like the listing going away.
1041     *
1042     * @param after_end - Is this auction already completed?
1043     */
1044    public void cancelSnipe(boolean after_end) {
1045      handleCancel(after_end);
1046  
1047      setMultiSnipe(null);
1048    }
1049  
1050    private void handleCancel(boolean after_end) {
1051      if(isSniped()) {
1052        JConfig.log().logDebug("Cancelling Snipe for: " + getTitle() + '(' + getIdentifier() + ')');
1053        setLastStatus("Cancelling snipe.");
1054        if(after_end) {
1055          mCancelSnipeBid = getSnipe().getAmount();
1056        }
1057      }
1058    }
1059  
1060    public void snipeCompleted() {
1061      setBid(getSnipe().getAmount());
1062      setBidQuantity(getSnipe().getQuantity());
1063      mNeedsUpdate = true;
1064      getSnipe().delete();
1065      setInteger("snipe_id", null);
1066      mSnipe = null;
1067      setDirty();
1068      saveDB();
1069    }
1070  
1071    /**
1072     * In this case, the snipe failed, and we want to cancel the snipe, but we
1073     * don't want to remove the listing from the multisnipe group, in case you
1074     * still win it.  (For example, if you have a bid on it already.)
1075     */
1076    public void snipeFailed() {
1077      handleCancel(true);
1078      mNeedsUpdate = true;
1079      setDirty();
1080      saveDB();
1081    }
1082  
1083    /**
1084     * @brief Completely update auction info from the server for this auction.
1085     */
1086    public void update() {
1087      mNeedsUpdate = false;
1088      mForceUpdate = false;
1089  
1090      // We REALLY don't want to leave an auction in the 'updating'
1091      // state.  It does bad things.
1092      try {
1093        getServer().reload(this);
1094      } catch(Exception e) {
1095        JConfig.log().handleException("Unexpected exception during auction reload/update.", e);
1096      }
1097      mLastUpdatedAt = System.currentTimeMillis();
1098      mAddedRecently = 0;
1099      try {
1100        updateHighBid();
1101        checkHighBidder();
1102      } catch(Exception e) {
1103        JConfig.log().handleException("Unexpected exception during high bidder check.", e);
1104      }
1105      checkSeller();
1106      //  TODO Move all this to 'setComplete' on 'true'...
1107      if (isComplete()) {
1108        onComplete();
1109      } else {
1110        Date serverTime = new Date(System.currentTimeMillis() +
1111                                   getServer().getServerTimeDelta());
1112  
1113        //  If we're past the end time, update once, and never again.
1114        if (serverTime.after(getEndDate())) {
1115          setComplete(true);
1116          mNeedsUpdate = true;
1117          mForceUpdate = true;
1118        }
1119      }
1120      saveDB();
1121    }
1122  
1123    private void onComplete() {//  If the auction is really completed now, and it was part of a
1124      //  multisnipe group, let's check if it's been won.  If it has,
1125      //  tell the MultiSnipe object that one has been won, so it can
1126      //  clear out the others!
1127      boolean won = isHighBidder() && (!isReserve() || isReserveMet());
1128      if (isMultiSniped()) {
1129        MultiSnipe ms = getMultiSnipe();
1130        if (won) {
1131          ms.setWonAuction(/* this */);
1132        } else {
1133          ms.remove(getIdentifier());
1134        }
1135      }
1136      if(won) {
1137        JConfig.increment("stats.won");
1138      }
1139      if (isSniped()) {
1140        //  It's okay to cancel the snipe here; if the auction was won, it would be caught above.
1141        setLastStatus("Cancelling snipe, auction is reported as ended.");
1142        cancelSnipe(true);
1143      }
1144    }
1145  
1146    public void prepareSnipe(Currency snipe) { prepareSnipe(snipe, 1); }
1147  
1148    /**
1149     * @brief Set up the fields necessary for a future snipe.
1150     *
1151     * This needs to be enhanced to work with multiple items, and
1152     * different snipe times.
1153     *
1154     * @param snipe The amount of money the user wishes to bid at the last moment.
1155     * @param quantity The number of items they want to snipe for.
1156     */
1157    public void prepareSnipe(Currency snipe, int quantity) {
1158      if(snipe == null || snipe.isNull()) {
1159        if(getSnipe() != null) {
1160          getSnipe().delete();
1161        }
1162        setInteger("snipe_id", null);
1163        mSnipe = null;
1164        getServer().cancelSnipe(getIdentifier());
1165      } else {
1166        mSnipe = AuctionSnipe.create(snipe, quantity, 0);
1167        getServer().setSnipe(this);
1168      }
1169      setDirty();
1170      saveDB();
1171      MQFactory.getConcrete("Swing").enqueue("SNIPECHANGED");
1172    }
1173  
1174    /**
1175     * @brief Refresh the snipe, so it picks up a potentially changed end time, or when initially loading items.
1176     */
1177    public void refreshSnipe() {
1178      getServer().cancelSnipe(getIdentifier());
1179      getServer().setSnipe(this);
1180    }
1181  
1182    /**
1183     * @brief Bid a given price on an arbitrary number of a particular item.
1184     *
1185     * @param bid - The amount of money being bid.
1186     * @param bidQuantity - The number of items being bid on.
1187     *
1188     * @return The result of the bid attempt.
1189     */
1190    public int bid(Currency bid, int bidQuantity) {
1191      setBid(bid);
1192      setBidQuantity(bidQuantity);
1193      mBidAt = System.currentTimeMillis();
1194  
1195      JConfig.log().logDebug("Bidding " + bid + " on " + bidQuantity + " item[s] of (" + getIdentifier() + ")-" + getTitle());
1196  
1197      int rval = getServer().bid(this, bid, bidQuantity);
1198      saveDB();
1199      return rval;
1200    }
1201  
1202    /**
1203     * @brief Buy an item directly.
1204     *
1205     * @param quant - The number of them to buy.
1206     *
1207     * @return The result of the 'Buy' attempt.
1208     */
1209    public int buy(int quant) {
1210      int rval = AuctionServerInterface.BID_ERROR_NOT_BIN;
1211      Currency bin = getBuyNow();
1212      if(bin != null && !bin.isNull()) {
1213        setBid(getBuyNow());
1214        setBidQuantity(quant);
1215        mBidAt = System.currentTimeMillis();
1216        JConfig.log().logDebug("Buying " + quant + " item[s] of (" + getIdentifier() + ")-" + getTitle());
1217        rval = getServer().buy(this, quant);
1218        saveDB();
1219      }
1220      return rval;
1221    }
1222  
1223    /**
1224     * @brief This auction entry needs to be updated.
1225     */
1226    public void setNeedsUpdate() { mNeedsUpdate = true; }
1227  
1228    /**
1229     * @brief Make this auction update despite being ended.
1230     *
1231     * Clear the 'dont update' flag for this, because this is always a
1232     * user-forced update message.
1233     */
1234    public void forceUpdate() { mForceUpdate = true; mDontUpdate = 0; mNeedsUpdate = true; }
1235  
1236    /**
1237     * @brief Get the category this belongs in, usually used for tab names, and fitting in search results.
1238     *
1239     * @return - A category, or null if none has been assigned.
1240     */
1241    public String getCategory() {
1242      if(mCategory == null) {
1243        String category_id = get("category_id");
1244        if(category_id != null) {
1245          mCategory = Category.findFirstBy("id", category_id);
1246        }
1247      }
1248      if(mCategory == null) {
1249        setCategory(!isComplete() ? (isSeller() ? "selling" : "current") : "complete");
1250      }
1251  
1252      return mCategory != null ? mCategory.getName() : null;
1253    }
1254  
1255    /**
1256     * @brief Set the category associated with the auction entry.  If the
1257     * auction is ended, this is automatically considered sticky.
1258     *
1259     * @param newCategory - The new category to associate this item with.
1260     */
1261    public void setCategory(String newCategory) {
1262      Category c = Category.findFirstByName(newCategory);
1263      if(c == null) {
1264        c = Category.findOrCreateByName(newCategory);
1265      }
1266      setInteger("category_id", c.getId());
1267      mCategory = c;
1268      if(isComplete()) setSticky(true);
1269      saveDB();
1270    }
1271  
1272    /**
1273     * @brief Returns whether or not this auction entry is 'sticky', i.e. sticks to any category it's set to.
1274     * Whether the 'category' information is sticky (i.e. overrides 'deleted', 'selling', etc.)
1275     *
1276     * @return true if the entry is sticky, false otherwise.
1277     */
1278    public boolean isSticky() { return getBoolean("sticky"); }
1279  
1280    /**
1281     * @brief Set the sticky flag on or off.
1282     *
1283     * This'll probably be exposed to the user through a right-click context menu, so that people
1284     * can make auctions not move from their sorted categories when they end.
1285     *
1286     * @param beSticky - Whether or not this entry should be sticky.
1287     */
1288    public void setSticky(boolean beSticky) { setBoolean("sticky", beSticky); saveDB(); }
1289  
1290    /**
1291     * @brief This auction entry does NOT need to be updated.
1292     */
1293    public void clearNeedsUpdate() {
1294      mNeedsUpdate = false;
1295      mLastUpdatedAt = System.currentTimeMillis();
1296    }
1297  
1298    /**
1299     * @brief Pause updating this item, including things like moving to
1300     * completed, etc.
1301     */
1302    public void pauseUpdate() {
1303      mDontUpdate = System.currentTimeMillis() + 5 * Constants.ONE_MINUTE;
1304    }
1305  
1306    /**
1307     * @brief Is this entry paused?
1308     *
1309     * @return - Whether updates for this item are paused.
1310     */
1311    public boolean isPaused() { return mDontUpdate != 0; }
1312  
1313    public static final String endedAuction = "Auction ended.";
1314    private static final String mf_min_sec = "{6}{2,number,##}m, {7}{3,number,##}s";
1315    private static final String mf_hrs_min = "{5}{1,number,##}h, {6}{2,number,##}m";
1316    private static final String mf_day_hrs = "{4}{0,number,##}d, {5}{1,number,##}h";
1317  
1318    private static final String mf_min_sec_detailed = "{6}{2,number,##} minute{2,choice,0#, |1#, |1<s,} {7}{3,number,##} second{3,choice,0#|1#|1<s}";
1319    private static final String mf_hrs_min_detailed = "{5}{1,number,##} hour{1,choice,0#, |1#, |1<s,} {6}{2,number,##} minute{2,choice,0#|1#|1<s}";
1320    private static final String mf_day_hrs_detailed = "{4}{0,number,##} day{0,choice,0#, |1#, |1<s,}  {5}{1,number,##} hour{1,choice,0#|1#|1<s}";
1321  
1322    //0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}
1323  
1324    private static String convertToMsgFormat(String simpleFormat) {
1325      String msgFmt = simpleFormat.replaceAll("DD", "{4}{0,number,##}");
1326      msgFmt = msgFmt.replaceAll("HH", "{5}{1,number,##}");
1327      msgFmt = msgFmt.replaceAll("MM", "{6}{2,number,##}");
1328      msgFmt = msgFmt.replaceAll("SS", "{7}{3,number,##}");
1329  
1330      return msgFmt;
1331    }
1332  
1333    /**
1334     * @brief Determine the amount of time left, and format it prettily.
1335     *
1336     * @return A nicely formatted string showing how much time is left
1337     * in this auction.
1338     */
1339    public String getTimeLeft() {
1340      long rightNow = System.currentTimeMillis();
1341      long officialDelta = getServer().getServerTimeDelta();
1342      long pageReqTime = getServer().getPageRequestTime();
1343  
1344      if(!isComplete()) {
1345        long dateDiff;
1346        try {
1347          dateDiff = getEndDate().getTime() - ((rightNow + officialDelta) - pageReqTime);
1348        } catch(Exception endDateException) {
1349          JConfig.log().handleException("Error getting the end date.", endDateException);
1350          dateDiff = 0;
1351        }
1352  
1353        if(dateDiff > Constants.ONE_DAY * 60) return "N/A";
1354  
1355        if(dateDiff >= 0) {
1356          long days = dateDiff / (Constants.ONE_DAY);
1357          dateDiff -= days * (Constants.ONE_DAY);
1358          long hours = dateDiff / (Constants.ONE_HOUR);
1359          dateDiff -= hours * (Constants.ONE_HOUR);
1360          long minutes = dateDiff / (Constants.ONE_MINUTE);
1361          dateDiff -= minutes * (Constants.ONE_MINUTE);
1362          long seconds = dateDiff / Constants.ONE_SECOND;
1363  
1364          String mf = getTimeFormatter(days, hours);
1365  
1366          Object[] timeArgs = { days,           hours,      minutes,     seconds,
1367                                pad(days), pad(hours), pad(minutes), pad(seconds) };
1368  
1369          return(MessageFormat.format(mf, timeArgs));
1370        }
1371      }
1372      return endedAuction;
1373    }
1374  
1375    private String getTimeFormatter(long days, long hours) {
1376      String mf;
1377      boolean use_detailed = JConfig.queryConfiguration("timeleft.detailed", "false").equals("true");
1378      String cfg;
1379      if(days == 0) {
1380        if(hours == 0) {
1381          mf = use_detailed?mf_min_sec_detailed:mf_min_sec;
1382          cfg = JConfig.queryConfiguration("timeleft.minutes");
1383          if(cfg != null) mf = convertToMsgFormat(cfg);
1384        } else {
1385          mf = use_detailed?mf_hrs_min_detailed:mf_hrs_min;
1386          cfg = JConfig.queryConfiguration("timeleft.hours");
1387          if (cfg != null) mf = convertToMsgFormat(cfg);
1388        }
1389      } else {
1390        mf = use_detailed?mf_day_hrs_detailed:mf_day_hrs;
1391        cfg = JConfig.queryConfiguration("timeleft.days");
1392        if (cfg != null) mf = convertToMsgFormat(cfg);
1393      }
1394      return mf;
1395    }
1396  
1397    private String pad(long x) {
1398      return (x < 10) ? " " : "";
1399    }
1400  
1401    public boolean isUpdateForced() { return mForceUpdate; }
1402  
1403    /**
1404     * @brief Do a 'standard' compare to another AuctionEntry object.
1405     *
1406     * The standard ordering is as follows:
1407     *    (if identifiers or pointers are equal, entries are equal)
1408     *    If this end date is after the passed in one, we are greater.
1409     *    If this end date is before, we are lesser.
1410     *    Otherwise (EXACTLY equal dates!), order by identifier.
1411     *
1412     * @param other - The AuctionEntry to compare to.
1413     *
1414     * @return - -1 for lesser, 0 for equal, 1 for greater.
1415     */
1416    public int compareTo(AuctionEntry other) {
1417      //  We are always greater than null
1418      if(other == null) return 1;
1419      //  We are always equal to ourselves
1420      if(other == this) return 0;
1421  
1422      String identifier = getIdentifier();
1423  
1424      //  If the identifiers are the same, we're equal.
1425      if(identifier != null && identifier.equals(other.getIdentifier())) return 0;
1426  
1427      if(getEndDate() == null && other.getEndDate() != null) return 1;
1428      if(getEndDate() != null && other.getEndDate() == null) return -1;
1429      if (getEndDate() != null && other.getEndDate() != null) {
1430        //  If this ends later than the passed in object, then we are 'greater'.
1431        if(getEndDate().after(other.getEndDate())) return 1;
1432        if(other.getEndDate().after(getEndDate())) return -1;
1433      }
1434  
1435      //  Whoops!  Dates are equal, down to the second probably, or both null...
1436  
1437      //  If this has a null identifier, we're lower.
1438      if(identifier == null && other.getIdentifier() != null) return -1;
1439      if(identifier == null && other.getIdentifier() == null) return 0;
1440      //  At this point, we know identifier != null, so if the compared entry
1441      //  has a null identifier, we sort higher.
1442      if(other.getIdentifier() == null) return 1;
1443  
1444      //  Since this ends exactly at the same time as another auction,
1445      //  check the identifiers (which *must* be different here.
1446      return getIdentifier().compareTo(other.getIdentifier());
1447    }
1448  
1449    /**
1450     * @brief Return a value that indicates the status via bitflags, so that sorted groups by status will show up grouped together.
1451     *
1452     * @return - An integer containing a bitfield of relevant status bits.
1453     */
1454    public int getFlags() {
1455      int r_flags = 1;
1456  
1457      if (isFixed()) r_flags = 0;
1458      if (getHighBidder() != null) {
1459        if (isHighBidder()) {
1460          r_flags = 2;
1461        } else if (isSeller() && getNumBidders() > 0 &&
1462                   (!isReserve() || isReserveMet())) {
1463          r_flags = 4;
1464        }
1465      }
1466      if (!getBuyNow().isNull()) {
1467        r_flags += 8;
1468      }
1469      if (isReserve()) {
1470        if (isReserveMet()) {
1471          r_flags += 16;
1472        } else {
1473          r_flags += 32;
1474        }
1475      }
1476      if(hasPaypal()) r_flags += 64;
1477      return r_flags;
1478    }
1479  
1480    public AuctionInfo getAuction() {
1481      if(mAuction == null) {
1482        String aid = get("auction_id");
1483        if(aid != null && aid.length() != 0) {
1484          mAuction = AuctionInfo.findFirstBy("id", aid);
1485        }
1486        if(mAuction == null && getString("identifier") != null) {
1487          mAuction = AuctionInfo.findByIdentifier(getString("identifier"));
1488        }
1489  
1490        //  If we successfully loaded an auction info object...
1491        if(mAuction != null) {
1492          setDefaultCurrency(mAuction.getDefaultCurrency());
1493  
1494          if(getString("identifier") == null) {
1495            setString("identifier", mAuction.getIdentifier());
1496            setInteger("auction_id", mAuction.getId());
1497            saveDB();
1498          }
1499        }
1500      }
1501  
1502      return mAuction;
1503    }
1504  
1505    /**
1506     * @brief Force this auction to use a particular set of auction
1507     * information for it's core data (like seller's name, current high
1508     * bid, etc.).
1509     *
1510     * @param inAI - The AuctionInfo object to make the new core data.  Must not be null.
1511     */
1512    public void setAuctionInfo(AuctionInfo inAI) {
1513      if(mAuction == null) {
1514        setDefaultCurrency(inAI.getDefaultCurrency());
1515      }
1516  
1517      //  If the end date has changed, let's reschedule the snipes for the new end date...?
1518      boolean doRefresh = (mAuction != null && mAuction.getEndDate() != null &&
1519          !mAuction.getEndDate().equals(inAI.getEndDate()) && getSnipe() != null);
1520  
1521      AuctionInfo oldAuction = mAuction;
1522      mAuction = inAI;
1523      String newAuctionId = mAuction.saveDB();
1524      if(doRefresh) refreshSnipe();
1525      if(newAuctionId != null) {
1526        set("auction_id", newAuctionId);
1527        setString("identifier", mAuction.getIdentifier());
1528        //  If we had an old auction, and it's not the same as the new one,
1529        //  and the IDs are different, delete the old one.
1530        if (oldAuction != null &&
1531            oldAuction != mAuction &&
1532            mAuction.getId() != null &&
1533            oldAuction.getId() != null &&
1534            !mAuction.getId().equals(oldAuction.getId())) {
1535          oldAuction.delete();
1536        }
1537      }
1538  
1539      checkHighBidder();
1540      checkSeller();
1541      checkEnded();
1542      saveDB();
1543    }
1544  
1545    ////////////////////////////////////////
1546    //  Passthrough functions to AuctionInfo
1547  
1548    /* Accessor functions that are passed through directly down
1549     * to the internal AuctionInfo object.
1550     */
1551    public Currency getCurBid() { return getAuction().getCurBid(); }
1552    public Currency getUSCurBid() { return getAuction().getUSCurBid(); }
1553    public Currency getMinBid() { return getAuction().getMinBid(); }
1554  
1555    /**
1556     * @return - Shipping amount, overrides AuctionInfo shipping amount if present.
1557     */
1558    public Currency getShipping() {
1559      if(!getMonetary("shipping").isNull()) return getMonetary("shipping");
1560      return getAuction().getShipping();
1561    }
1562    public Currency getInsurance() { return getAuction().getInsurance(); }
1563    public boolean getInsuranceOptional() { return getAuction().isInsuranceOptional(); }
1564    public Currency getBuyNow() { return getAuction().getBuyNow(); }
1565  
1566    public int getQuantity() { return getAuction().getQuantity(); }
1567    public int getNumBidders() { return getAuction().getNumBidders(); }
1568  
1569    public String getSeller() { return getAuction().getSellerName(); }
1570    public String getHighBidder() { return getAuction().getHighBidder(); }
1571    public String getTitle() { return getAuction().getTitle(); }
1572  
1573    public Date getStartDate() {
1574      if (getAuction() != null && getAuction().getStartDate() != null) {
1575        Date start = getAuction().getStartDate();
1576        if(start != null) return start;
1577      }
1578  
1579      return Constants.LONG_AGO;
1580    }
1581  
1582    public Date getEndDate() {
1583      if(getAuction() != null && getAuction().getEndDate() != null) {
1584        Date end = getAuction().getEndDate();
1585        if(end != null) return end;
1586      }
1587  
1588      return Constants.FAR_FUTURE;
1589    }
1590    public Date getSnipeDate() { return new Date(getAuction().getEndDate().getTime() - getSnipeTime()); }
1591  
1592    public boolean isDutch() { return getAuction().isDutch(); }
1593    public boolean isReserve() { return getAuction().isReserve(); }
1594    public boolean isReserveMet() { return getAuction().isReserveMet(); }
1595    public boolean isPrivate() { return getAuction().isPrivate(); }
1596    public boolean isFixed() { return getAuction().isFixedPrice(); }
1597  
1598    public StringBuffer getContent() { return getAuction().getContent(); }
1599    public File getContentFile() { return getAuction().getContentFile(); }
1600    public String getThumbnail() { return getAuction().getThumbnail(); }
1601  
1602    public boolean hasPaypal() { return getAuction().hasPaypal(); }
1603    public String getItemLocation() { return getAuction().getItemLocation(); }
1604    public String getPositiveFeedbackPercentage() { return getAuction().getPositiveFeedbackPercentage(); }
1605    public int getFeedbackScore() { return getAuction().getFeedbackScore(); }
1606  
1607    public void setErrorPage(StringBuffer page) { mLastErrorPage = page; }
1608    public StringBuffer getErrorPage() { return mLastErrorPage; }
1609  
1610    public Currency getShippingWithInsurance() {
1611      Currency ship = getShipping();
1612      if(ship == null || ship.isNull())
1613        return Currency.NoValue();
1614      else {
1615        if(getInsurance() != null &&
1616           !getInsurance().isNull() &&
1617           !getInsuranceOptional()) {
1618          try {
1619            ship = ship.add(getInsurance());
1620          } catch(Currency.CurrencyTypeException cte) {
1621            JConfig.log().handleException("Insurance is somehow a different type than shipping?!?", cte);
1622          }
1623        }
1624      }
1625      return ship;
1626    }
1627  
1628    public boolean isShippingOverridden() {
1629      Currency ship = getMonetary("shipping");
1630      return ship != null && !ship.isNull();
1631    }
1632  
1633    /**
1634     * Is the auction deleted on the server?
1635     *
1636     * @return - true if the auction has been removed from the server, as opposed to deleted locally.
1637     */
1638    public boolean isDeleted() {
1639      return getBoolean("deleted", false);
1640    }
1641  
1642    /**
1643     * Mark the auction as having been deleted by the auction server.
1644     *
1645     * Generally items are removed by the auction server because the listing is
1646     * too old, violates some terms of service, the seller has been suspended,
1647     * or the seller removed the listing themselves.
1648     */
1649    public void setDeleted() {
1650      if(!isDeleted()) {
1651        setBoolean("deleted", true);
1652        clearInvalid();
1653      } else {
1654        setComplete(true);
1655      }
1656      saveDB();
1657    }
1658  
1659    /**
1660     * Mark the auction as NOT having been deleted by the auction server.
1661     *
1662     * It's possible we mistakenly saw a server-error as a 404 (or they
1663     * presented it as such), so we need to be able to clear the deleted status.
1664     */
1665    public void clearDeleted() {
1666      if(isDeleted()) {
1667        setBoolean("deleted", false);
1668        saveDB();
1669      }
1670    }
1671  
1672    /**
1673     * @return - Has this auction already ended?  We keep track of this, so we
1674     * don't waste time on it afterwards, even as much as creating a
1675     * Date object, and comparing.
1676     */
1677    public boolean isComplete() { return getBoolean("ended"); }
1678    public void setComplete(boolean complete) { setBoolean("ended", complete); saveDB(); }
1679  
1680    /*************************/
1681    /* Database access stuff */
1682    /*************************/
1683  
1684    public String saveDB() {
1685      if(mAuction == null) return null;
1686  
1687      String auctionId = mAuction.saveDB();
1688      if(auctionId != null) set("auction_id", auctionId);
1689  
1690      //  This just makes sure we have a default category before saving.
1691      getCategory();
1692      if(mCategory != null) {
1693        String categoryId = mCategory.saveDB();
1694        if(categoryId != null) set("category_id", categoryId);
1695      }
1696  
1697      if(getSnipe() != null) {
1698        String snipeId = getSnipe().saveDB();
1699        if(snipeId != null) set("snipe_id", snipeId);
1700      }
1701  
1702      if(mEntryEvents != null) {
1703        mEntryEvents.save();
1704      }
1705  
1706      String id = super.saveDB();
1707      set("id", id);
1708      return id;
1709    }
1710  
1711    public boolean reload() {
1712      try {
1713        AuctionEntry ae = AuctionEntry.findFirstBy("id", get("id"));
1714        if (ae != null) {
1715          setBacking(ae.getBacking());
1716          mAuction = ae.getAuction();
1717          ae.getCategory();
1718          mCategory = ae.mCategory;
1719          mSnipe = ae.getSnipe();
1720          mEntryEvents = ae.getEvents();
1721          mMultiSnipe = ae.getMultiSnipe();
1722          return true;
1723        }
1724      } catch (Exception e) {
1725        //  Ignored - the reload semi-silently fails.
1726        JConfig.log().logDebug("reload from the database failed for (" + getIdentifier() + ")");
1727      }
1728      return false;
1729    }
1730  
1731  //  private static Table sDB = null;
1732    protected static String getTableName() { return "entries"; }
1733    protected Table getDatabase() {
1734      return getRealDatabase();
1735    }
1736  
1737    private static ThreadLocal<Table> tDB = new ThreadLocal<Table>() {
1738      protected synchronized Table initialValue() {
1739        return openDB(getTableName());
1740      }
1741    };
1742  
1743    public static Table getRealDatabase() {
1744      return tDB.get();
1745    }
1746  
1747    public static AuctionEntry findFirstBy(String key, String value) {
1748      return (AuctionEntry) ActiveRecord.findFirstBy(AuctionEntry.class, key, value);
1749    }
1750  
1751    @SuppressWarnings({"unchecked"})
1752    public static List<AuctionEntry> findActive() {
1753      String notEndedQuery = "SELECT * FROM entries WHERE (ended != 1 OR ended IS NULL)";
1754      return (List<AuctionEntry>) findAllBySQL(AuctionEntry.class, notEndedQuery);
1755    }
1756  
1757    @SuppressWarnings({"unchecked"})
1758    public static List<AuctionEntry> findEnded() {
1759      return (List<AuctionEntry>) findAllBy(AuctionEntry.class, "ended", "1");
1760    }
1761  
1762    @SuppressWarnings({"unchecked"})
1763    public static List<AuctionEntry> findAllSniped() {
1764      return (List<AuctionEntry>) findAllBySQL(AuctionEntry.class, "SELECT * FROM " + getTableName() + " WHERE (snipe_id IS NOT NULL OR multisnipe_id IS NOT NULL)");
1765    }
1766  
1767    @SuppressWarnings({"unchecked"})
1768    public static List<AuctionEntry> findAll() {
1769      return (List<AuctionEntry>) findAllBySQL(AuctionEntry.class, "SELECT * FROM entries");
1770    }
1771  
1772    public static int count() {
1773      return count(AuctionEntry.class);
1774    }
1775  
1776    public static int activeCount() {
1777      return getRealDatabase().countBy("(ended != 1 OR ended IS NULL)");
1778    }
1779  
1780    public static int completedCount() {
1781      return getRealDatabase().countBy("ended = 1");
1782    }
1783  
1784    public static int uniqueCount() {
1785      return getRealDatabase().countBySQL("SELECT COUNT(DISTINCT(identifier)) FROM entries WHERE identifier IS NOT NULL");
1786    }
1787  
1788    private static final String snipeFinder = "(snipe_id IS NOT NULL OR multisnipe_id IS NOT NULL) AND (ended != 1 OR ended IS NULL)";
1789  
1790    public static int snipedCount() {
1791      return getRealDatabase().countBy(snipeFinder);
1792    }
1793  
1794    public static AuctionEntry nextSniped() {
1795      String sql = "SELECT entries.* FROM entries, auctions WHERE " + snipeFinder + 
1796          " AND (entries.auction_id = auctions.id) ORDER BY auctions.ending_at ASC";
1797      return (AuctionEntry) findFirstBySQL(AuctionEntry.class, sql);
1798    }
1799  
1800    /**
1801     * Locate an AuctionEntry by first finding an AuctionInfo with the passed
1802     * in auction identifier, and then looking for an AuctionEntry which
1803     * refers to that AuctionInfo row.
1804     *
1805     * @param identifier - The auction identifier to search for.
1806     * @return - null indicates that the auction isn't in the database yet,
1807     * otherwise an AuctionEntry will be loaded and returned.
1808     */
1809    public static AuctionEntry findByIdentifier(String identifier) {
1810      AuctionEntry ae = findFirstBy("identifier", identifier);
1811  
1812      if(ae != null) {
1813        if(ae.getAuction() == null) {
1814          JConfig.log().logMessage("Error loading auction #" + identifier + ", entry found, auction missing.");
1815          ae = null;
1816        }
1817      }
1818  
1819      if(ae == null) {
1820        AuctionInfo ai = AuctionInfo.findByIdentifier(identifier);
1821        if(ai != null) {
1822          ae = AuctionEntry.findFirstBy("auction_id", ai.getString("id"));
1823          if (ae != null) ae.setAuctionInfo(ai);
1824        }
1825      }
1826  
1827      return ae;
1828    }
1829  
1830    public static boolean deleteAll(List<AuctionEntry> toDelete) {
1831      if(toDelete.isEmpty()) return true;
1832  
1833      String entries = makeCommaList(toDelete);
1834      List<AuctionInfo> auctions = new ArrayList<AuctionInfo>();
1835      List<MultiSnipe> multisnipes = new ArrayList<MultiSnipe>();
1836      List<AuctionSnipe> snipes = new ArrayList<AuctionSnipe>();
1837  
1838      for(AuctionEntry entry : toDelete) {
1839        auctions.add(entry.getAuction());
1840        if(entry.isSniped()) snipes.add(entry.getSnipe());
1841      }
1842  
1843      boolean success = new EventStatus().deleteAllEntries(entries);
1844      if(!snipes.isEmpty()) success &= AuctionSnipe.deleteAll(snipes);
1845      if(!multisnipes.isEmpty()) success &= MultiSnipe.deleteAll(multisnipes);
1846      success &= AuctionInfo.deleteAll(auctions);
1847      success &= toDelete.get(0).getDatabase().deleteBy("id IN (" + entries + ")");
1848  
1849      return success;
1850    }
1851  
1852    public boolean delete() {
1853      if(getAuction() != null) getAuction().delete();
1854      if(getSnipe() != null) getSnipe().delete();
1855      return super.delete();
1856    }
1857  
1858    public static final String newRow = "<tr><td>";
1859    public static final String newCol = "</td><td>";
1860    public static final String endRow = "</td></tr>";
1861  
1862    // TODO -- Extract this crap out to a EntryHTMLBuilder class, which gets instantiated with an AuctionEntry object.
1863    public String buildInfoHTML() {
1864      return buildInfoHTML(false);
1865    }
1866  
1867    public String buildInfoHTML(boolean forRSS) {
1868      String prompt = "";
1869  
1870      if(forRSS) {
1871        prompt += "<b>" + StringTools.stripHigh(getTitle()) + "</b> (" + getIdentifier() + ")<br>";
1872      } else {
1873        prompt += "<b>" + getTitle() + "</b> (" + getIdentifier() + ")<br>";
1874      }
1875      prompt += "<table>";
1876      boolean addedThumbnail = false;
1877      if(getThumbnail() != null) {
1878        if (forRSS) {
1879          try {
1880            InetAddress thisIp = InetAddress.getLocalHost();
1881            prompt += newRow + "<img src=\"http://" + thisIp.getHostAddress() + ":" + JConfig.queryConfiguration("server.port", Constants.DEFAULT_SERVER_PORT_STRING) + "/" + getIdentifier() + ".jpg\">" + newCol + "<table>";
1882            addedThumbnail = true;
1883          } catch (UnknownHostException e) {
1884            //  Couldn't find THIS host?!?  Perhaps that means we're not online?
1885            JConfig.log().logMessage("Unknown host trying to look up the local host.  Is the network off?");
1886          }
1887        } else {
1888          prompt += newRow + "<img src=\"" + getThumbnail() + "\">" + newCol + "<table>";
1889          addedThumbnail = true;
1890        }
1891      }
1892      prompt = buildInfoBody(prompt, addedThumbnail);
1893  
1894    	return(prompt);
1895    }
1896  
1897    private String buildRow(String label, Object value) {
1898      return newRow + label + newCol + value.toString() + endRow;
1899    }
1900  
1901    private String buildInfoBody(String prompt, boolean addedThumbnail) {
1902      if(!isFixed()) {
1903        prompt += buildRow("Currently", getCurBid() + " (" + getNumBidders() + " Bids)");
1904        prompt += buildRow("High bidder", getHighBidder());
1905      } else {
1906        prompt += buildRow("Price", getCurBid());
1907      }
1908      if(isDutch()) {
1909        prompt += buildRow("Quantity", getQuantity());
1910      }
1911  
1912      if(isBidOn()) {
1913        prompt += buildRow("Your max bid", getBid());
1914        if(getBidQuantity() != 1) {
1915          prompt += buildRow("Quantity of", getBidQuantity());
1916        }
1917      }
1918  
1919      if(isSniped()) {
1920        prompt += buildRow("Sniped for", getSnipeAmount());
1921        if(getSnipeQuantity() != 1) {
1922          prompt += buildRow("Quantity of", getSnipeQuantity());
1923        }
1924        prompt += newRow + "Sniping at " + (getSnipeTime() / 1000) + " seconds before the end." + endRow;
1925      }
1926  
1927      if(getShipping() != null && !getShipping().isNull()) {
1928        prompt += buildRow("Shipping", getShipping());
1929      }
1930      if(!getInsurance().isNull()) {
1931        prompt += buildRow("Insurance (" + (getInsuranceOptional()?"optional":"required") + ")", getInsurance());
1932      }
1933      prompt += buildRow("Seller", getSeller());
1934      if(isComplete()) {
1935        prompt += buildRow("Listing ended at ", getEndDate());
1936      } else {
1937        prompt += buildRow("Listing ends at", getEndDate());
1938      }
1939      if(addedThumbnail) {
1940        prompt += "</table>" + endRow;
1941      }
1942      prompt += "</table>";
1943  
1944      if(!isFixed() && !getBuyNow().isNull()) {
1945        if(isComplete()) {
1946          prompt += "<b>You could have used Buy It Now for " + getBuyNow() + "</b><br>";
1947        } else {
1948          prompt += "<b>Or you could buy it now, for " + getBuyNow() + ".</b><br>";
1949          prompt += "Note: <i>To 'Buy Now' through this program,<br>      select 'Buy from the context menu.</i><br>";
1950        }
1951      }
1952  
1953      if(isComplete()) {
1954        prompt += "<i>Listing has ended.</i><br>";
1955      }
1956  
1957      if(getComment() != null) {
1958        prompt += "<br><u>Comment</u><br>";
1959  
1960        prompt += "<b>" + getComment() + "</b><br>";
1961      }
1962  
1963      prompt += "<b><u>Events</u></b><blockquote>" + getStatusHistory() + "</blockquote>";
1964      return prompt;
1965    }
1966  
1967    public String buildHTMLComment(boolean showThumbnail) {
1968      boolean hasComment = (getComment() != null);
1969      boolean hasThumb = showThumbnail && (getThumbnail() != null);
1970  
1971      if(JConfig.queryConfiguration("display.thumbnail", "true").equals("false")) hasThumb = false;
1972      if(!hasComment && !hasThumb) return null;
1973  
1974      StringBuffer wholeHTML = new StringBuffer("<html><body>");
1975      if(hasThumb && hasComment) {
1976        wholeHTML.append("<table><tr><td><img src=\"").append(getThumbnail()).append("\"></td><td>").append(getComment()).append("</td></tr></table>");
1977      } else {
1978        if(hasThumb) {
1979          wholeHTML.append("<img src=\"").append(getThumbnail()).append("\">");
1980        } else {
1981          wholeHTML.append(getComment());
1982        }
1983      }
1984      wholeHTML.append("</body></html>");
1985  
1986      return wholeHTML.toString();
1987    }
1988  
1989    public static void setResolver(Resolver resolver) {
1990      sResolver = resolver;
1991    }
1992  
1993    public static XMLElement retrieveAuctionXML(String identifier) {
1994      AuctionEntry ae = AuctionEntry.construct(identifier);
1995      if (ae != null) {
1996        return ae.toXML(); //  TODO -- Check high bidder (a separate request).
1997      }
1998  
1999      return null;
2000    }
2001  
2002    public static StringBuffer retrieveAuctionXMLString(String identifier) {
2003      XMLElement xe = retrieveAuctionXML(identifier);
2004  
2005      return xe != null ? xe.toStringBuffer() : null;
2006    }
2007  
2008    //  Debugging method, to test multisnipe cancelling.
2009    public void win() {
2010      MultiSnipe ms = getMultiSnipe();
2011      ms.setWonAuction(/* this */);
2012    }
2013  
2014    public void setUpdating() {
2015      mUpdating = true;
2016    }
2017  
2018    public void clearUpdating() {
2019      mUpdating = false;
2020    }
2021  
2022    public static int countByCategory(Category c) {
2023      if(c == null) return 0;
2024      return getRealDatabase().countBySQL("SELECT COUNT(*) FROM entries WHERE category_id=" + c.getId());
2025    }
2026  
2027    public static List<AuctionEntry> findAllBy(String column, String value) {
2028      return (List<AuctionEntry>)ActiveRecord.findAllBy(AuctionEntry.class, column, value);
2029    }
2030  
2031    public void setNumBids(int bidCount) {
2032      mAuction.setNumBids(bidCount);
2033    }
2034  }

Check out the code: svn co jbidwatcher/trunk/src/com/jbidwatcher/auction/AuctionEntry.java