X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~yarrgweb/git?p=jarrg-ian.git;a=blobdiff_plain;f=src%2Fnet%2Fchiark%2Fyarrg%2FMarketUploader.java;h=980d5de1b527f0b2968e54196ce8292e98d060d6;hp=4e78282de05079d205058e1ba38d35879118aaf6;hb=9a0ebbeb9b39aa6ed93c419a306f60267a1d0cc9;hpb=56a013ad992b3c8463ce43d7f73d724bb16448f5 diff --git a/src/net/chiark/yarrg/MarketUploader.java b/src/net/chiark/yarrg/MarketUploader.java index 4e78282..980d5de 100644 --- a/src/net/chiark/yarrg/MarketUploader.java +++ b/src/net/chiark/yarrg/MarketUploader.java @@ -21,35 +21,42 @@ import java.util.regex.*; import java.util.prefs.Preferences; import java.beans.*; -/** -* MarketUploader is a class that handles the uploading of market -* data from Yohoho! Puzzle Pirates via the Java Accessibility -* API. -* -* MarketUploader initializes after the main YPP window has -* initialized. It provides a simple window with a "Capture -* Market Data" button displayed. Upon clicking this button, a -* progress dialog is displayed, and the data is processed and -* submitted to the YARRG and PCTB servers. If any errors occur, -* an error dialog is shown, and processing returns, the button -* becoming re-enabled. -*/ +/* + * MarketUploader is a class that handles the uploading of market + * data from Yohoho! Puzzle Pirates via the Java Accessibility + * API. + * + * MarketUploader initializes after the main YPP window has + * initialized. It provides a simple window with a "Capture + * Market Data" button displayed. Upon clicking this button, a + * progress dialog is displayed, and the data is processed and + * submitted to the YARRG and PCTB servers. If any errors occur, + * an error dialog is shown, and processing returns, the button + * becoming re-enabled. + */ public class MarketUploader implements Runnable, TopLevelWindowListener, GUIInitializedListener { + // UI object references which are set during startup private JFrame frame = null; private Window window = null; + + // Genuinely global variables + private PrintStream dtxt = null; + public int uploadcounter = 0; + + // UI objects which are enabled/disabled, cleared/set, created/destroyed, + // etc., for each upload private JButton findMarket = null; private JLabel resultSummary = null; private JLabel arbitrageResult = null; - private int unknownPCTBcommods = 0; - private long startTime = 0; private ProgressMonitor progmon = null; + // PCTB protocol parameters private final static String PCTB_LIVE_HOST_URL = "http://pctb.crabdance.com/"; private final static String PCTB_TEST_HOST_URL = "http://pctb.ilk.org/"; private String PCTB_HOST_URL; - // Yarrg protocol parameters + // YARRG protocol parameters private final static String YARRG_CLIENTNAME = "jpctb greenend"; private final static String YARRG_CLIENTVERSION = net.chiark.yarrg.Version.version; @@ -60,17 +67,29 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { "http://upload.yarrg.chiark.net/test/commod-update-receiver"; private String YARRG_URL; + // Preferences private boolean uploadToYarrg; private boolean uploadToPCTB; private boolean showArbitrage; + // Values cleared/set for each upload, or used during upload processing + private long startTime = 0; + private String islandName = null; private String oceanName = null; private java.util.concurrent.CountDownLatch latch = null; private AccessibleContext sidePanel; + + // PCTB-specific variables + private int unknownPCTBcommods = 0; private HashMap commodMap; - public PrintStream dtxt = null; + + + /***************************************** + * UPLOAD-TARGET-INDEPENDENT CODE * + *****************************************/ + /* * UTILITY METHODS AND SUBCLASSES @@ -89,7 +108,8 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { private void debuglog(String s) { if (dtxt == null) return; long now = new Date().getTime(); - dtxt.println("progress "+(now - startTime)+"ms "+s); + dtxt.println("progress "+(now - startTime)+"ms " + +Thread.currentThread().getName()+": "+s); } private void debug_write_stringdata(String what, String data) @@ -107,164 +127,19 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { strm.write(data); strm.close(); } - - /** - * An abstract market offer, entailing a commodity being bought or sold by - * a shoppe, for a certain price in a certain quantity. Not instantiable. - * - * @see Buy - * @see Sell - */ - abstract class Offer { - public int commodity, price, quantity, shoppe; - /** - * Create an offer from record, determining the shoppe Id from - * stallMap and the commodity Id from commodMap. - * priceIndex should be the index of the price in the record - * (the quantity will be priceIndex + 1). - * - * @param record the record with data to create the offer from - * @param stallMap a map containing the ids of the various stalls - * @param commodMap a map containing the ids of the various commodities - * @param priceIndex the index of the price in the record - */ - public Offer(ArrayList record, - LinkedHashMap stallMap, - HashMap commodMap, - int priceIndex) { - Integer commodId = commodMap.get(record.get(0)); - if(commodId == null) { - throw new IllegalArgumentException(); - } - commodity = commodId.intValue(); - price = Integer.parseInt(record.get(priceIndex)); - String qty = record.get(priceIndex+1); - quantity = parseQty(qty); - shoppe = stallMap.get(record.get(1)).intValue(); - } - - /** - * Returns a human-readable version of this offer, useful for debugging - * - * @return human-readable offer - */ - public String toString() { - return "[C:" + commodity + ",$" + price + ",Q:" - + quantity + ",S:" + shoppe + "]"; - } - } - - /** - * An offer from a shoppe or stall to buy a certain quantity of a - * commodity for a certain price. If placed in an ordered Set, - * sorts by commodity index ascending, then by buy price - * descending, and finally by stall id ascending. - */ - class Buy extends Offer implements Comparable { - /** - * Creates a new Buy offer from the given - * record using the other parameters to determine - * stall id and commodity id of the offer. - * - * @param record the record with data to create the offer from - * @param stallMap a map containing the ids of the various stalls - * @param commodMap a map containing the ids of the various commodities - */ - public Buy(ArrayList record, - LinkedHashMap stallMap, - HashMap commodMap) { - super(record,stallMap,commodMap,2); - } - - /** - * Sorts by commodity index ascending, then price descending, - * then stall id ascending. - */ - public int compareTo(Buy buy) { - // organize by: commodity index, price, stall index - if(commodity == buy.commodity) { - // organize by price, then by stall index - if(price == buy.price) { - // organize by stall index - return shoppe>buy.shoppe ? 1 : -1; - } else if(price > buy.price) { - return -1; - } else { - return 1; - } - } else if(commodity > buy.commodity) { - return 1; - } else { - return -1; - } - } - } - - /** - * An offer from a shoppe or stall to sell a certain quantity of - * a commodity for a certain price. If placed in an ordered Set, - * sorts by commodity index ascending, then by sell price - * ascending, and finally by stall id ascending. - */ - class Sell extends Offer implements Comparable { - /** - * Creates a new Sell offer from the given - * record using the other parameters to determine - * stall id and commodity id of the offer. - * - * @param record the record with data to create the offer from - * @param stallMap a map containing the ids of the various stalls - * @param commodMap a map containing the ids of the various commodities - */ - public Sell(ArrayList record, - LinkedHashMap stallMap, - HashMap commodMap) { - super(record,stallMap,commodMap,4); - } - - /** - * Sorts by commodity index ascending, then price ascending, then - * stall id ascending. - */ - public int compareTo(Sell sell) { - // organize by: commodity index, price, stall index - if(commodity == sell.commodity) { - // organize by price, then by stall index - if(price == sell.price) { - // organize by stall index - return shoppe>sell.shoppe ? 1 : -1; - } else if(price > sell.price) { - return 1; - } else { - return -1; - } - } else if(commodity > sell.commodity) { - return 1; - } else { - return -1; - } - } - } - private void progressNote(final String s_in) throws Exception { - new UIX() { public void body() { - String arb = null; - arb = arbitrageResult.getText(); - String s = s_in; - if (arb != null && arb.length() != 0) - s = "" + arb + "
" + s; - progmon.setNote(s); - }}.exec("progressNote"); - } /* * ENTRY POINT AND STARTUP * * Main thread and/or event thread */ - /* - * Entry point. Read our preferences. - */ + + public static void main(String[] args) { + // This is not normally called, it seems. + new MarketUploader(); + } + public MarketUploader() { Preferences prefs = Preferences.userNodeForPackage(getClass()); @@ -294,7 +169,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { /* * We arrange to wait for the GUI to be initialised, then look at - * every top-level window, and if it + * every top-level window to see if the Puzzle Pirates window turns up. */ public void run() { debuglog("MarketUploader run()..."); @@ -325,8 +200,9 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { // already got it return; String name = w.getAccessibleContext().getAccessibleName(); - debuglog("MarketUploader new toplevel "+name); + debuglog("MarketUploader checking toplevel "+name); if (!name.equals("Puzzle Pirates")) + // Only if we're running alongside a Window named "Puzzle Pirates" return; debuglog("MarketUploader found toplevel, creating gui"); window = w; @@ -334,12 +210,8 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { frame.setVisible(true); } - /** - * Set up the GUI, with its window and one-button - * interface. Only initialize if we're running alongside - * a Window named "Puzzle Pirates" though. - */ private void createGUI() { + // Actually set up our GUI on_ui_thread(); frame = new JFrame("Jarrg Uploader"); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); @@ -350,16 +222,19 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { findMarket = new JButton("Upload Market Data"); findMarket.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + // This is called when the user clicks "upload" on_ui_thread(); + + uploadcounter++; findMarket.setEnabled(false); resultSummary.setText(""); arbitrageResult.setText(""); - new Thread() { + new Thread("MarketUploader-uploader-"+uploadcounter) { public void run() { startTime = new Date().getTime(); unknownPCTBcommods = 0; try { - runUpload(); + runUpload(uploadcounter); } catch(Exception e) { error(e.toString()); e.printStackTrace(); @@ -399,12 +274,60 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { } /* - * ERROR REPORTING AND GENERAL UTILITIES + * THREAD HANDLING * - * Synchronous modal dialogues - * error and error_html may be called from any thread - */ + * Special measures are needed because: + * - it is not permitted to use any Swing UI classes or objects + * other than on the Swing event thread + * - we want to run our upload asynchronously + * - we want to do some computation asynchronously (eg, the + * arbitrage and upload data prep) + * + * So what we do is this: + * 1. When the user asks to upload, we spawn a new thread + * to do the upload ("MarketUploader-uploader-*", see + * the call to "new Thread" inside createGUI. + * 2. Whenever that thread needs to touch a UI object it + * uses EventQueue.invokeLater or .invokeAndWait to + * perform the relevant action. We wrap these calls up + * in three utility classes: + * UIA - runs code on UI thread, asynchronously + * UIX - runs code on UI thread, waits for it to finish + * UIXR - as UIX but also returns a value + * These hide the details of the EventQueue class and also do + * some debugging and argument shuffling; the calling syntax is + * still painful, unfortunately, and there is a weird constraint + * on variables used inside the inner body. For a simple + * example, see the handling of "summary" and "summary_final" + * for the call to UIX at the bottom of runUpload. + * 3. Try to put everything back when that thread exits. + * + * Additionally: + * a. There is another thread spawed early to get a timestamp from + * YARRG, if we are uploading there. + * b. Finding the island name can involve callbacks which run in + * the UI event thread. Basically we do the work there, and use + * a CountDownLatch to cause the uploader thread to wait as + * appropriate. + */ + + private void on_ui_thread() { assert(EventQueue.isDispatchThread()); } + private void on_our_thread() { assert(!EventQueue.isDispatchThread()); } + private abstract class UIA implements Runnable { + private String what; + public abstract void body(); + public void run() { + debuglog("UIA 2 "+what+" begin"); + body(); + debuglog("UIA 3 "+what+" done"); + } + public void exec(String what_in) { + what = what_in; + debuglog("UIA 1 "+what+" request"); + EventQueue.invokeLater(this); + } + }; private abstract class UIXR implements Runnable { public abstract ReturnType bodyr(); public ReturnType return_value; @@ -413,7 +336,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { debuglog("UIX 2 "+what+" begin"); return_value = bodyr(); debuglog("UIX 3 "+what+" done"); - }; + } public ReturnType exec(String what_in) throws Exception { what = what_in; if (EventQueue.isDispatchThread()) { @@ -426,17 +349,21 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { debuglog("UIX 4 "+what+" (other thread) exit"); } return return_value; - }; + } }; private abstract class UIX extends UIXR implements Runnable { public abstract void body(); public Object bodyr() { body(); return null; } }; - private void on_ui_thread() { assert(EventQueue.isDispatchThread()); } - private void on_our_thread() { assert(!EventQueue.isDispatchThread()); } + /* + * ERROR REPORTING AND GENERAL UTILITIES + * + * Synchronous modal dialogues + * error and error_html may be called from any thread + */ - private void error(final String msg) { + public void error(final String msg) { try { new UIX() { public void body() { resultSummary.setText("failed"); @@ -449,7 +376,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { } } - private void error_html(final String msg, String html) { + public void error_html(final String msg, String html) { Pattern body = Pattern.compile("(.*)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); Matcher m = body.matcher(html); @@ -468,124 +395,39 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { error(whole_msg); } - private void setProgress(final int nv) throws Exception { - new UIX() { public void body() { + public void progressNote(final String s_in) throws Exception { + new UIA() { public void body() { + String arb = null; + arb = arbitrageResult.getText(); + String s = s_in; + if (arb != null && arb.length() != 0) + s = "" + arb + "
" + s; + progmon.setNote(s); + }}.exec("progressNote "+s_in); + } + public void setProgress(final int nv) throws Exception { + new UIA() { public void body() { progmon.setProgress(nv); - }}.exec("setProgress"); + }}.exec("setProgress "+nv); } - private boolean isCanceled() throws Exception { + public boolean checkCancelled() throws Exception { return new UIXR() { public Boolean bodyr() { - return new Boolean(progmon.isCanceled()); - }}.exec("isCanceled").booleanValue(); + boolean can = progmon.isCanceled(); + if (can) resultSummary.setText("cancelled"); + return new Boolean(can); + }}.exec("checkCancelled").booleanValue(); } - - /* - * GUI MANIPULATION CALLBACKS - */ - - private PropertyChangeListener changeListener = new PropertyChangeListener() { - public void propertyChange(PropertyChangeEvent e) { - on_ui_thread(); - if(e.getNewValue() != null && - e.getPropertyName().equals - (AccessibleContext.ACCESSIBLE_CHILD_PROPERTY)) { - Accessible islandInfo = - descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0,0,1,2});; - String text = islandInfo.getAccessibleContext().getAccessibleText() - .getAtIndex(AccessibleText.SENTENCE,0); - int index = text.indexOf(":"); - String name = text.substring(0,index); - islandName = name; - // debuglog(islandName); - sidePanel.removePropertyChangeListener(this); - latch.countDown(); - } - } - }; - private void getIsland() { - on_ui_thread(); - - // If the league tracker is there, we can skip the faff - // and ask for its tooltip, since we're on a boat - Accessible leagueTrackerContainer = - descendNodes(window,new int[] {0,1,0,0,2,1}); - Accessible leagueTrackerItself = - descendByClass(leagueTrackerContainer, - "com.threerings.yohoho.sea.client.LeagueTracker"); - Accessible leagueTracker = descend(leagueTrackerItself, 1); - try { - islandName = ((JLabel)leagueTracker).getToolTipText(); - } catch (NullPointerException e) { - // evidently we're actually on an island - - islandName = null; - AccessibleContext chatArea = - descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2}) - .getAccessibleContext(); - // attach the property change listener to the outer sunshine - // panel if the "ahoy" tab is not active, otherwise attach it to - // the scroll panel in the "ahoy" tab. - if(!"com.threerings.piracy.client.AttentionListPanel". - equals(descendNodes(window,new int[] {0,1,0,0,2,2,0}) - .getClass().getCanonicalName())) { - sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2}) - .getAccessibleContext(); - } else { - sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0}) - .getAccessibleContext(); - } - sidePanel.addPropertyChangeListener(changeListener); - latch = new java.util.concurrent.CountDownLatch(1); - // make the Players Online ("/who") panel appear - AccessibleEditableText chat = chatArea.getAccessibleEditableText(); - chat.setTextContents("/w"); - int c = chatArea.getAccessibleAction().getAccessibleActionCount(); - for(int i=0;i buys = new TreeSet(); @@ -733,7 +575,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { LinkedHashMap stallMap = getStallMap(data); setProgress(40); progressNote("PCTB: Sorting offers"); - if(isCanceled()) { + if(checkCancelled()) { return; } // get commod map @@ -751,7 +593,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); setProgress(60); progressNote("PCTB: Sending data"); - if(isCanceled()) { + if(checkCancelled()) { return; } GZIPOutputStream out = new GZIPOutputStream(outStream); @@ -770,7 +612,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { debuglog("pctb sent."); if (in == null) return; setProgress(80); - if(isCanceled()) { + if(checkCancelled()) { return; } progressNote("PCTB: Waiting ..."); @@ -796,81 +638,180 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { resultSummary.setText(summary_final); }}.exec("resultSummary.setText"); - debuglog("done."); - } - - /** - * Get the offer data out of the table and cache it in an - * ArrayList. - * - * @param table the AccessibleTable containing the market data - * @return an array of record arrays, each representing a row of the table - */ - private ArrayList> getData(AccessibleTable table) { - on_ui_thread(); - ArrayList> data = new ArrayList>(); - for (int i = 0; i < table.getAccessibleRowCount(); i++) { - ArrayList row = new ArrayList(); - for (int j = 0; j < table.getAccessibleColumnCount(); j++) { - row.add(table.getAccessibleAt(i, j) - .getAccessibleContext().getAccessibleName()); + debuglog("done."); + } + + /* + * UPLOAD HELPER FUNCTIONS FOR EXTRACTING SPECIFIC UI DATA + */ + + private ArrayList> getData(AccessibleTable table) { + // Gets the offer data out of the table and returns it as an ArrayList + + on_ui_thread(); + ArrayList> data = new ArrayList>(); + for (int i = 0; i < table.getAccessibleRowCount(); i++) { + ArrayList row = new ArrayList(); + for (int j = 0; j < table.getAccessibleColumnCount(); j++) { + row.add(table.getAccessibleAt(i, j) + .getAccessibleContext().getAccessibleName()); + } + data.add(row); + } + return data; + } + + public AccessibleTable findMarketTable() { + // Return the table containing market data if it exists, otherwise null. + on_ui_thread(); + + Accessible node1 = window; + Accessible node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,0,1,0,0}); + // commod market + // commod market: {0,1,0,0,0,0,1,0,0,1,0} {0,1,0,0,0,0,1,0,1,0,0,1,0,0}) + // debuglog(node); + if (!(node instanceof JTable)) { + node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,1,0,0,1,0,0}); + // commod market + } + if (!(node instanceof JTable)) return null; + AccessibleTable table = node.getAccessibleContext().getAccessibleTable(); + // debuglog(table); + return table; + } + + private boolean isDisplayAll() { + // Returns true iff the "Display:" menu on the commodities + // interface in YPP is set to "All" + on_ui_thread(); + + Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1}); + if(!(button instanceof JButton)) { + button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1}); + } + String display = button.getAccessibleContext().getAccessibleName(); + if(!display.equals("All")) { + return false; + } + return true; + } + + /* + * FUNCTIONS AND CALLBACKS FOR FINDING ISLAND AND OCEAN + */ + + private void getOcean() { + // Finds the ocean name from the window title. + // Stores it in the global oceanName + on_ui_thread(); + + oceanName = null; + AccessibleContext topwindow = window.getAccessibleContext(); + oceanName = topwindow.getAccessibleName() + .replaceAll(".*on the (\\w+) ocean", "$1"); + } + + private void getIsland() { + // Tries to find the island name. Either: + // (a) sets the islandName global + // or + // (b) sets latch to a new CountDownLatch, and arranges that + // at some point later, islandName will be set and the latch + // decremented to zero + on_ui_thread(); + + // If the league tracker is there, we can skip the faff + // and ask for its tooltip, since we're on a boat + + Accessible leagueTrackerContainer = + descendNodes(window,new int[] {0,1,0,0,2,1}); + Accessible leagueTrackerItself = + descendByClass(leagueTrackerContainer, + "com.threerings.yohoho.sea.client.LeagueTracker"); + Accessible leagueTracker = descend(leagueTrackerItself, 1); + try { + islandName = ((JLabel)leagueTracker).getToolTipText(); + latch = null; + } catch (NullPointerException e) { + // evidently we're actually on an island + + islandName = null; + AccessibleContext chatArea = + descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2}) + .getAccessibleContext(); + // attach the property change listener to the outer sunshine + // panel if the "ahoy" tab is not active, otherwise attach it to + // the scroll panel in the "ahoy" tab. + if(!"com.threerings.piracy.client.AttentionListPanel". + equals(descendNodes(window,new int[] {0,1,0,0,2,2,0}) + .getClass().getCanonicalName())) { + sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2}) + .getAccessibleContext(); + } else { + sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0}) + .getAccessibleContext(); + } + sidePanel.addPropertyChangeListener(changeListener); + latch = new java.util.concurrent.CountDownLatch(1); + // make the Players Online ("/who") panel appear + AccessibleEditableText chat = chatArea.getAccessibleEditableText(); + chat.setTextContents("/w"); + int c = chatArea.getAccessibleAction().getAccessibleActionCount(); + for(int i=0;inull - */ - public AccessibleTable findMarketTable() { - on_ui_thread(); - Accessible node1 = window; - Accessible node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,0,1,0,0}); - // commod market - // commod market: {0,1,0,0,0,0,1,0,0,1,0} {0,1,0,0,0,0,1,0,1,0,0,1,0,0}) - // debuglog(node); - if (!(node instanceof JTable)) { - node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,1,0,0,1,0,0}); - // commod market + + private PropertyChangeListener changeListener = new PropertyChangeListener() { + // used by getIsland + public void propertyChange(PropertyChangeEvent e) { + on_ui_thread(); + if(e.getNewValue() != null && + e.getPropertyName().equals + (AccessibleContext.ACCESSIBLE_CHILD_PROPERTY)) { + Accessible islandInfo = + descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0,0,1,2});; + String text = islandInfo.getAccessibleContext().getAccessibleText() + .getAtIndex(AccessibleText.SENTENCE,0); + int index = text.indexOf(":"); + String name = text.substring(0,index); + islandName = name; + // debuglog(islandName); + sidePanel.removePropertyChangeListener(this); + latch.countDown(); + } } - if (!(node instanceof JTable)) return null; - AccessibleTable table = node.getAccessibleContext().getAccessibleTable(); - // debuglog(table); - return table; - } - - /** - * Utility method to descend through several levels of Accessible children - * at once. + }; + + /* + * UTILITY FUNCTIONS FOR WALKING THE UI * - * @param parent the node on which to start the descent - * @param path an array of ints, each int being the index of the next - * accessible child to descend. - * @return the Accessible reached by following the - * descent path, or null if the desired path was - * invalid. + * These functions all return null if the specified path or child + * was not found. */ + private Accessible descendNodes(Accessible parent, int[] path) { + // Descends through several levels of Accessible children in one call. + // path[] is an array of ints, each int being the index into the array + // of children at a particular point, and thus selects the specific + // accessible child to descend to. on_ui_thread(); + for(int i=0;iAccessible "node". - * - * @param parent the node with children - * @param childNum the index of the child of parent to return - * @return the childNum child of parent - * or null if the child is not found. - */ + private Accessible descend(Accessible parent, int childNum) { + // Descends one level to the specified child of the parent + // childNum is the index of the child within parent on_ui_thread(); + if (parent == null) return null; int children = parent.getAccessibleContext().getAccessibleChildrenCount(); if (childNum >= children) { @@ -883,15 +824,10 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { return child; } - /** - * Descends one level to the child which has the specified class. - * - * @param parent the node with children - * @param classname the name of the class, as a string - * @return the child or null if the child is not found. - */ private Accessible descendByClass(Accessible parent, String classname) { + // Descends one level to the first child which has the specified class. on_ui_thread(); + if (parent == null) return null; AccessibleContext ac = parent.getAccessibleContext(); int children = ac.getAccessibleChildrenCount(); @@ -905,31 +841,150 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { debuglog("DESCEND CLASS "+classname+" NOT FOUND"); return null; } + - public static void main(String[] args) { - new MarketUploader(); - } + /***************************************** + * PCTB-SPECIFIC HELPER FUNCTIONS ETC. * + *****************************************/ /** - * Returns true if the "Display:" menu on the commodities - * interface in YPP is set to "All" + * An abstract market offer, entailing a commodity being bought or sold by + * a shoppe, for a certain price in a certain quantity. Not instantiable. * - * @return true if all commodities are displayed, - * otherwise false + * @see Buy + * @see Sell */ - private boolean isDisplayAll() { - on_ui_thread(); - Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1}); - if(!(button instanceof JButton)) { - button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1}); + abstract class Offer { + public int commodity, price, quantity, shoppe; + /** + * Create an offer from record, determining the shoppe Id from + * stallMap and the commodity Id from commodMap. + * priceIndex should be the index of the price in the record + * (the quantity will be priceIndex + 1). + * + * @param record the record with data to create the offer from + * @param stallMap a map containing the ids of the various stalls + * @param commodMap a map containing the ids of the various commodities + * @param priceIndex the index of the price in the record + */ + public Offer(ArrayList record, + LinkedHashMap stallMap, + HashMap commodMap, + int priceIndex) { + Integer commodId = commodMap.get(record.get(0)); + if(commodId == null) { + throw new IllegalArgumentException(); + } + commodity = commodId.intValue(); + price = Integer.parseInt(record.get(priceIndex)); + String qty = record.get(priceIndex+1); + quantity = parseQty(qty); + shoppe = stallMap.get(record.get(1)).intValue(); } - String display = button.getAccessibleContext().getAccessibleName(); - if(!display.equals("All")) { - return false; + + /** + * Returns a human-readable version of this offer, useful for debugging + * + * @return human-readable offer + */ + public String toString() { + return "[C:" + commodity + ",$" + price + ",Q:" + + quantity + ",S:" + shoppe + "]"; + } + } + + /** + * An offer from a shoppe or stall to buy a certain quantity of a + * commodity for a certain price. If placed in an ordered Set, + * sorts by commodity index ascending, then by buy price + * descending, and finally by stall id ascending. + */ + class Buy extends Offer implements Comparable { + /** + * Creates a new Buy offer from the given + * record using the other parameters to determine + * stall id and commodity id of the offer. + * + * @param record the record with data to create the offer from + * @param stallMap a map containing the ids of the various stalls + * @param commodMap a map containing the ids of the various commodities + */ + public Buy(ArrayList record, + LinkedHashMap stallMap, + HashMap commodMap) { + super(record,stallMap,commodMap,2); + } + + /** + * Sorts by commodity index ascending, then price descending, + * then stall id ascending. + */ + public int compareTo(Buy buy) { + // organize by: commodity index, price, stall index + if(commodity == buy.commodity) { + // organize by price, then by stall index + if(price == buy.price) { + // organize by stall index + return shoppe>buy.shoppe ? 1 : -1; + } else if(price > buy.price) { + return -1; + } else { + return 1; + } + } else if(commodity > buy.commodity) { + return 1; + } else { + return -1; + } } - return true; } + /** + * An offer from a shoppe or stall to sell a certain quantity of + * a commodity for a certain price. If placed in an ordered Set, + * sorts by commodity index ascending, then by sell price + * ascending, and finally by stall id ascending. + */ + class Sell extends Offer implements Comparable { + /** + * Creates a new Sell offer from the given + * record using the other parameters to determine + * stall id and commodity id of the offer. + * + * @param record the record with data to create the offer from + * @param stallMap a map containing the ids of the various stalls + * @param commodMap a map containing the ids of the various commodities + */ + public Sell(ArrayList record, + LinkedHashMap stallMap, + HashMap commodMap) { + super(record,stallMap,commodMap,4); + } + + /** + * Sorts by commodity index ascending, then price ascending, then + * stall id ascending. + */ + public int compareTo(Sell sell) { + // organize by: commodity index, price, stall index + if(commodity == sell.commodity) { + // organize by price, then by stall index + if(price == sell.price) { + // organize by stall index + return shoppe>sell.shoppe ? 1 : -1; + } else if(price > sell.price) { + return 1; + } else { + return -1; + } + } else if(commodity > sell.commodity) { + return 1; + } else { + return -1; + } + } + } + /** * Gets the list of commodities and their associated commodity ids. * @@ -1279,6 +1334,11 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { } } + + /***************************************** + * YARRG-SPECIFIC HELPER FUNCTIONS ETC. * + *****************************************/ + private InputStream post_for_yarrg(ClientHttpRequest http) throws IOException { on_our_thread(); @@ -1290,6 +1350,21 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { return http.resultstream(); } + private class YarrgTimestampFetcher extends Thread { + public YarrgTimestampFetcher(int counter) { + super("MarketUploader-YarrgTimestampFetcher-"+uploadcounter); + } + public String ts = null; + public void run() { + try { + ts = getYarrgTimestamp(); + debuglog("(async) yarrg timestamp ready."); + } catch(Exception e) { + error("Error getting YARRG timestamp: "+e); + } + } + }; + private String getYarrgTimestamp() throws IOException { ClientHttpRequest http = new ClientHttpRequest (YARRG_URL); http.setParameter("clientname", YARRG_CLIENTNAME); @@ -1368,6 +1443,10 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener { return profit; } + /***************************************** + * ARBITRAGE * + *****************************************/ + private class arbitrageOfferComparator implements Comparator { public int compare(Object o1, Object o2) { int p1 = ((int[])o1)[0];