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.
- ~
- jbidwatcher
- trunk
- src
- com
- jbidwatcher
- auction
- AuctionEntry.java
| 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
