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