package com.jbidwatcher.auction.server;
/*
* Copyright (c) 2000-2007, CyberFOX Software, Inc. All Rights Reserved.
*
* Developed by mrs (Morgan Schweers)
*/
/*
* @file AuctionServer.java
* @author Morgan Schweers <cyberfox@jbidwatcher.com>
* @date Wed Oct 9 13:49:02 2002
* @note Library GPL'ed.
* @brief This is an interface description for the general auction Servers
*
* It allows abstracting the auction setup to a factory creator, so
* the factory can identify which site (ebay, yahoo, amazon, etc.) it
* is, and do the appropriate parsing for that site.
*/
import com.jbidwatcher.search.SearchManager;
import com.jbidwatcher.util.*;
import com.jbidwatcher.util.Currency;
import com.jbidwatcher.util.config.*;
import com.jbidwatcher.util.queue.MQFactory;
import com.jbidwatcher.util.queue.AuctionQObject;
import com.jbidwatcher.util.http.CookieJar;
import com.jbidwatcher.util.http.Http;
import com.jbidwatcher.search.SearchManagerInterface;
import com.jbidwatcher.auction.*;
import com.jbidwatcher.scripting.Scripting;
import java.util.*;
import java.net.*;
import java.io.*;
public abstract class AuctionServer implements AuctionServerInterface {
private static long sLastUpdated = 0;
protected EntryCorral entryCorral;
protected SearchManager searcher;
private static class ReloadItemException extends Exception { }
/**
* Just a friendlier wrapper for the pair.
*/
private class ParseResults extends Pair<AuctionInfo, ItemParser.ParseErrors> {
private ParseResults(AuctionInfo info, ItemParser.ParseErrors parseErrors) { super(info, parseErrors); }
}
// Note: JBidProxy
public abstract CookieJar getNecessaryCookie(boolean force);
// UI functions
// Note: AuctionServerManager
public abstract ServerMenu establishMenu();
// Note: AuctionServerManager
public abstract void cancelSearches();
public abstract void addSearches(SearchManagerInterface searchManager);
// Exposed to AuctionEntry for checking high bidder status.
public abstract void updateHighBid(String ae);
public abstract void setSnipe(String auctionId);
public abstract void cancelSnipe(String identifier);
/**
* @brief Get the string-form URL for a given item ID on this
* auction server, for when we aren't browsing.
*
* Note: AuctionEntry, JBWDropHandler
*
* @param itemID - The item to retrieve the URL for.
*
* @return - A String with the full URL of the item description on the auction server.
*/
public abstract String getStringURLFromItem(String itemID);
/**
* @brief Allocate a new auction that is of this auction server's specific subtype.
*
* In order for the auctions to be able to fill out relevant
* information, each has to have a subclass of AuctionInfo dedicated
* to it. This abstract function is defined by the auction server
* specific classes, and actually returns an object of their type of
* 'auction'.
*
* Note: AuctionEntry
*
* @return - A freshly allocated auction-server specific auction data object.
*/
public abstract SpecificAuction getNewSpecificAuction();
/**
* @brief Get the official 'right now' time from the server.
*
* @return - The current time as reported by the auction site's 'official time' mechanism.
*/
protected abstract Date getOfficialTime();
/**
* Get the full text of an auction from the auction server.
*
* @param id - The item id for the item to retrieve from the server.
* @return - The full text of the auction from the server, or null if it wasn't found.
* @throws java.io.FileNotFoundException - If the auction is gone from the server.
*/
protected abstract StringBuffer getAuction(String id) throws FileNotFoundException;
protected abstract ItemParser getItemParser(StringBuffer sb, AuctionEntry ae, String item_id);
protected abstract String getUserId();
/**
* @brief Get the current time inline with the current thread. This will
* block until it's done getting the time.
*/
public void reloadTime() {
if (getOfficialTime() != null) {
MQFactory.getConcrete("Swing").enqueue("Successfully synchronized time with " + getFriendlyName() + '.');
JConfig.log().logMessage("Time delta with " + getFriendlyName() + " is " + getServerTimeDelta());
} else {
MQFactory.getConcrete("Swing").enqueue("Failed to synchronize time with " + getFriendlyName() + '!');
}
}
// Generalized logic
// -----------------
// Note: AuctionEntry
public AuctionInfo create(String itemId) {
return load(itemId, null);
}
/**
* @brief Load an auction from a given URL, and return the textual
* form of that auction to the caller in a Stringbuffer, having
* passed in any necessary cookies, passwords, etc.
*
* @param auctionURL - The URL of the auction to load.
*
* @return - A StringBuffer containing the text of the auction at that URL.
*
* @throws java.io.FileNotFoundException -- If the URL doesn't exist on the auction server.
*/
public StringBuffer getAuction(URL auctionURL) throws FileNotFoundException {
if(auctionURL == null) return null;
StringBuffer loadedPage;
try {
CookieJar curCook = getNecessaryCookie(false);
URLConnection uc;
if(curCook != null) {
uc = curCook.connect(auctionURL.toString());
} else {
uc = Http.net().makeRequest(auctionURL, null);
}
loadedPage = Http.net().receivePage(uc);
if(loadedPage != null && loadedPage.length() == 0) {
loadedPage = null;
}
} catch(FileNotFoundException fnfe) {
JConfig.log().logDebug("Item not found: " + auctionURL.toString());
throw fnfe;
} catch(IOException e) {
JConfig.log().handleException("Error loading URL (" + auctionURL.toString() + ')', e);
loadedPage = null;
}
return loadedPage;
}
/**
* @brief Given an auction entry, reload/update the core auction information from the server.
*
* Note: AuctionEntry
*
* @param auctionId
* @return - The core auction information that has been set into the
* auction entry, or null if the update failed.
*/
public AuctionInfo reload(String auctionId) {
AuctionEntry ae = (AuctionEntry) entryCorral.takeForWrite(auctionId);
try {
AuctionInfo ai = ae.getAuction();
Map<String, Object> r = rubyUpdate(auctionId, ae.getLastUpdated());
if (r != null && !r.isEmpty()) {
ai.setMonetary("curBid", Currency.getCurrency((String) r.get("current_price")));
ai.setBoolean("ended", (Boolean) r.get("ended"));
ai.setNumBids(((Long) r.get("bid_count")).intValue());
ai.setDate("end", new Date((Long) r.get("end_date")));
ai.saveDB();
ae.setAuctionInfo(ai);
ae.clearInvalid();
MQFactory.getConcrete("Swing").enqueue("LINK UP");
return ai;
} else {
AuctionInfo curAuction;
curAuction = load(auctionId, ae);
if (curAuction != null) {
curAuction.saveDB();
ae.setAuctionInfo(curAuction);
ae.clearInvalid();
MQFactory.getConcrete("Swing").enqueue("LINK UP");
return curAuction;
} else {
if (!ae.isDeleted() && !ae.getLastStatus().contains("Seller away - item unavailable.")) {
ae.setLastStatus("Failed to load from server!");
ae.setInvalid();
}
return null;
}
}
} finally {
entryCorral.release(auctionId);
}
}
public ParseResults parseAuction(ItemParser itemParser, AuctionEntry ae) {
String sellerName = null;
SpecificAuction auction = getNewSpecificAuction();
AuctionInfo output = auction;
if (ae != null) {
output = ae.getAuction();
sellerName = ae.getSellerName();
}
Record parse = itemParser.parseItemDetails();
ItemParser.ParseErrors setResult = auction.setFields(parse, sellerName);
if (setResult == ItemParser.ParseErrors.SELLER_AWAY) {
if (ae != null) {
ae.setLastStatus("Seller away - item unavailable.");
}
}
if (setResult == ItemParser.ParseErrors.SUCCESS) {
if(output != auction) {
Record newBacking = auction.getBacking();
for (String key : newBacking.keySet()) {
output.set(key, newBacking.get(key));
}
}
}
return new ParseResults(output, setResult);
}
public AuctionInfo doParse(StringBuffer sb) throws ReloadItemException {
return doParse(sb, null, null);
}
public Map<String, Object> rubyUpdate(String auctionId, Date lastUpdatedAt) {
long before = System.currentTimeMillis();
try {
Map<String, Object> maps = (Map<String, Object>) Scripting.rubyMethod("get_update", auctionId, lastUpdatedAt);
if (maps != null) {
String timerLog = "Ruby took " + (System.currentTimeMillis() - before) + "ms";
JConfig.log().logMessage(timerLog);
return maps;
}
} catch (Exception e) {
JConfig.log().logMessage("Could not parse easy-update. Using complex update.");
}
String timerLog = "Ruby took " + (System.currentTimeMillis() - before) + "ms, and failed.";
JConfig.log().logMessage(timerLog);
return null;
}
public Record tryRuby(StringBuffer sb) {
long before = System.currentTimeMillis();
Record rubyResults = null;
try {
Map<String, String> maps = (Map<String, String>) Scripting.rubyMethod("parse", sb.toString());
if (maps != null) {
rubyResults = new Record();
rubyResults.putAll(maps);
}
} catch (Exception e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
System.out.println("Ruby took " + (System.currentTimeMillis() - before));
return rubyResults;
}
/**
* Check to see if the provided user name is the current app user.
*
* @param username - The username to check.
* @return - false if username is null, or if the current user is the 'default' user, or if the username provided is different
* than the current username. True if the current app user is the same as the username passed in.
*/
public boolean isCurrentUser(String username) {
return !(username == null || isDefaultUser()) && getUserId().trim().equalsIgnoreCase(username.trim());
}
/**
* @param itemID - The eBay item ID to get a net.URL for.
* @return - a URL to use to pull that item.
* @brief Given a site-dependant item ID, get the URL for that item.
*/
protected URL getURLFromItem(String itemID) {
return StringTools.getURLFromString(getStringURLFromItem(itemID));
}
/**
* @brief Load an auction, given its item id, and its 'AuctionEntry'.
*
* @param item_id - The item # to associate this returned info with.
* @param ae - An object to notify when an error occurs.
*
* @return - An object containing the information extracted from the auction.
*/
private AuctionInfo load(String item_id, AuctionEntry ae) {
StringBuffer sb = null;
AuctionInfo curAuction = null;
int runCount = 0;
// Retry loop
while(sb == null && runCount < 2) {
sb = retrieveAuction(item_id, ae);
// If there was a failure retrieving the HTML itself, we probably got a 404.
if(sb == null && (ae == null || ae.isDeleted())) {
if(ae != null) ae.saveDB();
return null;
}
try {
curAuction = doParse(sb, ae, item_id);
} catch (ReloadItemException e) {
sb = null;
}
runCount++;
}
if (curAuction == null) {
JConfig.log().logMessage("Multiple failures attempting to load item " + item_id + ", giving up.");
JConfig.getMetrics().trackEventValue("item", "loadfailure", item_id);
if (ae != null && ae.getLastStatus().contains("Seller away - item unavailable.")) {
ae.setInvalid();
} else if (ae == null || !ae.isDeleted()) {
noteRetrieveError(ae);
}
}
return curAuction;
}
private StringBuffer retrieveAuction(String item_id, AuctionEntry ae) {
StringBuffer sb = null;
try {
sb = getAuction(item_id);
if(sb != null && ae != null) {
ae.clearDeleted();
}
} catch (FileNotFoundException ignored) {
// Just get out. The item no longer exists on the auction
// server, so we shouldn't be trying any of the rest. The
// Error should have been logged at the lower level, so just
// punt. It's not a communications error, either.
markAuctionDeleted(ae);
} catch (Exception catchall) {
if (JConfig.debugging()) {
JConfig.log().handleException("Some unexpected error occurred during loading the auction.", catchall);
}
}
return sb;
}
private AuctionInfo doParse(StringBuffer sb, AuctionEntry ae, String item_id) throws ReloadItemException {
ItemParser itemParser = getItemParser(sb, ae, item_id);
ParseResults parse = parseAuction(itemParser, ae);
AuctionInfo curAuction = parse.getFirst();
if(parse.getLast() == ItemParser.ParseErrors.SUCCESS) { // HAPPY PATH!
// Override the detected item id, if one was provided.
if(item_id != null) {
curAuction.setIdentifier(item_id);
} else if(ae != null && ae.getIdentifier() != null) {
curAuction.setIdentifier(ae.getIdentifier());
}
curAuction.setContent(sb, false);
curAuction.save();
} else {
String error = checkParseError(ae, parse.getLast());
JConfig.log().logMessage(error);
if (ae == null || !ae.isDeleted() && parse.getLast() != ItemParser.ParseErrors.SELLER_AWAY) {
checkLogError(ae);
}
curAuction = null;
}
return curAuction;
}
private String checkParseError(AuctionEntry ae, ItemParser.ParseErrors result) throws ReloadItemException {
String error = null;
switch(result) {
case WRONG_SITE: {
JConfig.log().logMessage("Attempted to read an auction that is not available on the default site; check eBay's non-US configuration.");
error = "Auction is not available on the used site.";
break;
}
case CAPTCHA: {
JConfig.log().logDebug("Failed to load (likely adult) item, captcha intervened.");
if(ae != null) {
ae.setLastStatus("Couldn't access auction on server; captcha blocked.");
}
error = "Couldn't access auction on server; captcha blocked.";
break;
}
case NOT_ADULT: {
boolean isAdult = JConfig.queryConfiguration(getName() + ".mature", "false").equals("true");
if (isAdult) {
getNecessaryCookie(true);
throw new ReloadItemException();
} else {
JConfig.log().logDebug("Failed to load adult item, user possibly not marked for Mature Items access. Check your eBay configuration.");
error = "Failed to load mature audiences item.";
}
break;
}
case DELETED: {
error = markAuctionDeleted(ae);
break;
}
case SELLER_AWAY: {
error = "Seller away - item unavailable.";
break;
}
case BAD_TITLE: {
error = "There was a problem parsing the title.";
break;
}
}
if(result != ItemParser.ParseErrors.SUCCESS && error == null) {
error = "Bad Parse!";
}
return error;
}
private String markAuctionDeleted(AuctionEntry ae) {
String error = "Auction appears to have been removed from the site.";
if(ae != null) {
ae.setDeleted();
error = "Auction " + ae.getIdentifier() + " appears to have been removed from the site.";
ae.setLastStatus(error);
}
return error;
}
private void noteRetrieveError(AuctionEntry ae) {
checkLogError(ae);
// Whoops! Bad thing happened on the way to loading the auction!
JConfig.log().logDebug("Failed to parse auction! Bad return result from auction server.");
// Only retry the login cookie once every ten minutes of these errors.
if ((sLastUpdated + Constants.ONE_MINUTE * 10) > System.currentTimeMillis()) {
sLastUpdated = System.currentTimeMillis();
MQFactory.getConcrete(this.getFriendlyName()).enqueueBean(new AuctionQObject(AuctionQObject.MENU_CMD, UPDATE_LOGIN_COOKIE, null)); //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* If we had an auction entry, note the failure on it's record.
* Otherwise, note a general communications failure.
*
* @param ae - The optional auction entry.
*/
private void checkLogError(AuctionEntry ae) {
if (ae != null) {
ae.logError();
} else {
MQFactory.getConcrete("Swing").enqueue("LINK DOWN Communications failure talking to the server");
}
}
public String stripId(String source) {
String strippedId = source;
if (source.startsWith("http")) {
strippedId = extractIdentifierFromURLString(source);
}
return strippedId;
}
}