chiark / gitweb /
style: add a comment about main() not being called
[jarrg-ian.git] / src / net / chiark / yarrg / MarketUploader.java
index 4e78282de05079d205058e1ba38d35879118aaf6..980d5de1b527f0b2968e54196ce8292e98d060d6 100644 (file)
@@ -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<String,Integer> 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 <code>record</code>, determining the shoppe Id from
-     * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
-     * <code>priceIndex</code> should be the index of the price in the record
-     * (the quantity will be <code>priceIndex + 1</code>).
-     *
-     * @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<String> record, 
-                LinkedHashMap<String,Integer> stallMap, 
-                HashMap<String,Integer> 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<Buy> {
-    /**
-     * Creates a new <code>Buy</code> offer from the given
-     * <code>record</code> 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<String> record,
-              LinkedHashMap<String,Integer> stallMap,
-              HashMap<String,Integer> 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<Sell> {
-    /**
-     * Creates a new <code>Sell</code> offer from the given
-     * <code>record</code> 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<String> record,
-               LinkedHashMap<String,Integer> stallMap,
-               HashMap<String,Integer> 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 = "<html>" + arb + "<br>" + 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<ReturnType> 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<Object> 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("<body>(.*)</body>",
                                   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 = "<html>" + arb + "<br>" + 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<Boolean>() { 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<c;i++) {
-       if("notify-field-accept".equals(chatArea.getAccessibleAction()
-                                       .getAccessibleActionDescription(i))) {
-         chatArea.getAccessibleAction().doAccessibleAction(i);
-       }
-      }
-    }
-  }
-
-  /**
-   *      Find the ocean name from the window title, and set global
-   *      oceanName variable
+  /*
+   * ACTUAL DATA COLLECTION AND UPLOAD
    */
-  private void getOcean() {
-    on_ui_thread();
-    oceanName = null;
-    AccessibleContext topwindow = window.getAccessibleContext();
-    oceanName = topwindow.getAccessibleName()
-      .replaceAll(".*on the (\\w+) ocean", "$1");
-  }
 
+  private void runUpload(int counter) throws Exception {
+    // Runs the data collection process, and upload the results.
+    // In most cases of error, we call error() (which synchronously
+    // reports the error) and then simply return.
 
-  /**
-   *   Run the data collection process, and upload the results. This
-   *   is the method that calls most of the other worker methods for
-   *   the process. If an error occurs, the method will call the
-   *   error method and return early, freeing up the button to be
-   *   clicked again.
-   *
-   *   @exception Exception if an error we didn't expect occured
-   */
-  private class YarrgTimestampFetcher extends Thread {
-    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 void runUpload() throws Exception {
     on_our_thread();
 
     boolean doneyarrg = false, donepctb = false;
@@ -595,7 +437,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
 
     if (uploadToYarrg) {
       debuglog("(async) yarrg timestamp...");
-      yarrgts_thread = new YarrgTimestampFetcher();
+      yarrgts_thread = new YarrgTimestampFetcher(counter);
       yarrgts_thread.start();
     }
 
@@ -725,7 +567,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       debuglog("pctb prepare...");
       progressNote("PCTB: Getting stall names");
       setProgress(20);
-      if(isCanceled()) {
+      if(checkCancelled()) {
        return;
       }
       TreeSet<Offer> buys = new TreeSet<Offer>();
@@ -733,7 +575,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       LinkedHashMap<String,Integer> 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
-   *   <code>ArrayList</code>.
-   *   
-   *   @param table the <code>AccessibleTable</code> containing the market data
-   *   @return an array of record arrays, each representing a row of the table
-   */
-  private ArrayList<ArrayList<String>> getData(AccessibleTable table) {
-    on_ui_thread();
-    ArrayList<ArrayList<String>> data = new ArrayList<ArrayList<String>>();
-    for (int i = 0; i < table.getAccessibleRowCount(); i++) {
-      ArrayList<String> row = new ArrayList<String>();
-      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<ArrayList<String>> getData(AccessibleTable table) {
+    // Gets the offer data out of the table and returns it as an ArrayList
+
+    on_ui_thread();
+    ArrayList<ArrayList<String>> data = new ArrayList<ArrayList<String>>();
+    for (int i = 0; i < table.getAccessibleRowCount(); i++) {
+      ArrayList<String> row = new ArrayList<String>();
+      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;i<c;i++) {
+       if("notify-field-accept".equals(chatArea.getAccessibleAction()
+                                       .getAccessibleActionDescription(i))) {
+         chatArea.getAccessibleAction().doAccessibleAction(i);
+       }
       }
-      data.add(row);
     }
-    return data;
   }
-       
-  /**
-   *   @return the table containing market data if it exists,
-   *   otherwise <code>null</code>
-   */
-  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 <code>Accessible</code> reached by following the
-   *   descent path, or <code>null</code> 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;i<path.length;i++) {
       if (null == (parent = descend(parent, path[i]))) return null;
     }
     return parent;
   }
-       
-  /**
-   *   Descends one level to the specified child of the parent
-   *   <code>Accessible</code> "node".
-   *   
-   *   @param parent the node with children
-   *   @param childNum the index of the child of <code>parent</code> to return
-   *   @return the <code>childNum</code> child of <code>parent</code>
-   *   or <code>null</code> 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 <code>null</code> 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 <code>true</code> if all commodities are displayed,
-   *   otherwise <code>false</code>
+   *   @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 <code>record</code>, determining the shoppe Id from
+     * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
+     * <code>priceIndex</code> should be the index of the price in the record
+     * (the quantity will be <code>priceIndex + 1</code>).
+     *
+     * @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<String> record, 
+                LinkedHashMap<String,Integer> stallMap, 
+                HashMap<String,Integer> 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<Buy> {
+    /**
+     * Creates a new <code>Buy</code> offer from the given
+     * <code>record</code> 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<String> record,
+              LinkedHashMap<String,Integer> stallMap,
+              HashMap<String,Integer> 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<Sell> {
+    /**
+     * Creates a new <code>Sell</code> offer from the given
+     * <code>record</code> 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<String> record,
+               LinkedHashMap<String,Integer> stallMap,
+               HashMap<String,Integer> 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];