Revision: 7706
Author: [email protected]
Date: Thu Mar 11 09:45:33 2010
Log: Reduce requests to finance server
Highlight search terms in stock names
Clean up some regex handling
Fix checkstyle problems
Merge with Joel's changes

http://code.google.com/p/google-web-toolkit/source/detail?r=7706

Added:
/trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/HighlightingTextCell.java /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/GoogleFinance.java
Modified:
 /trunk/bikeshed/.classpath
 /trunk/bikeshed/src/com/google/gwt/bikeshed/cells/client/EllipsisCell.java
/trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/StockSample.gwt.xml /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/Columns.java /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/FavoritesWidget.java /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
 /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/common.css
/trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/StockServiceImpl.java /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/Stocks.java /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockQuote.java /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockRequest.java /trunk/bikeshed/src/com/google/gwt/sample/expenses/domain/RelationshipValidationVisitor.java /trunk/bikeshed/src/com/google/gwt/sample/expenses/shared/EmployeeRequests.java /trunk/bikeshed/src/com/google/gwt/sample/expenses/shared/ReportRequests.java
 /trunk/bikeshed/test/com/google/gwt/sample/expenses/domain/StorageTest.java
 /trunk/bikeshed/war/Stocks.css

=======================================
--- /dev/null
+++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/HighlightingTextCell.java Thu Mar 11 09:45:33 2010
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.bikeshed.sample.stocks.client;
+
+import com.google.gwt.bikeshed.cells.client.Cell;
+import com.google.gwt.regexp.shared.MatchResult;
+import com.google.gwt.regexp.shared.RegExp;
+
+/**
+ * A {...@link Cell} used to render text, with portions matching a given
+ * regular expression highlighted.
+ */
+public class HighlightingTextCell extends Cell<String> {
+
+  private String highlightRegex;
+
+  @Override
+  public void render(String value, StringBuilder sb) {
+ sb.append("<div style='overflow:hidden; white-space:nowrap; text-overflow:ellipsis;'>");
+    if (highlightRegex == null || highlightRegex.length() == 0) {
+      sb.append(value);
+      sb.append("</div>");
+      return;
+    }
+
+    RegExp regExp = RegExp.compile(highlightRegex, "gi");
+    int fromIndex = 0;
+    int length = value.length();
+    MatchResult result;
+    while (fromIndex < length) {
+      // Find the next match of the highlight regex
+      result = regExp.exec(value);
+      if (result == null) {
+        // No more matches
+        break;
+      }
+      int index = result.getIndex();
+      String match = result.getGroup(0);
+
+      // Append the characters leading up to the match
+      sb.append(value.substring(fromIndex, index));
+      // Append the match in boldface
+      sb.append("<b>");
+      sb.append(match);
+      sb.append("</b>");
+      // Skip past the matched string
+      fromIndex = index + match.length();
+      regExp.setLastIndex(fromIndex);
+    }
+    // Append the tail of the string
+    if (fromIndex < length) {
+      sb.append(value.substring(fromIndex));
+    }
+    sb.append("</div>");
+  }
+
+  public void setHighlightRegex(String highlightRegex) {
+    this.highlightRegex = highlightRegex;
+  }
+}
=======================================
--- /dev/null
+++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/GoogleFinance.java Thu Mar 11 09:45:33 2010
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.bikeshed.sample.stocks.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A class to perform queries against the Google Finance server.
+ */
+public class GoogleFinance {
+
+  private static final Pattern DATA_PATTERN =
+    Pattern.compile("\"([^\"]*)\"\\s*:\\s*\"([^\"]*)\"");
+
+ private static final Pattern QUOTE_PATTERN = Pattern.compile("\\{[^\\}]*\\}");
+
+  public static void queryServer(Set<String> symbolsInRange,
+      Map<String, StockServiceImpl.Quote> quotes) {
+    // Build the URL string.
+    StringBuilder sb = new StringBuilder(
+    "http://www.google.com/finance/info?client=ig&q=";);
+    boolean first = true;
+    for (String symbol : symbolsInRange) {
+      if (!first) {
+        sb.append(',');
+      }
+      sb.append(symbol);
+      first = false;
+    }
+
+    // Send the request.
+    String content = "";
+    try {
+      String urlString = sb.toString();
+      URL url = new URL(urlString);
+      InputStream urlInputStream = url.openStream();
+      Scanner contentScanner = new Scanner(urlInputStream, "UTF-8");
+      if (contentScanner.hasNextLine()) {
+ // See http://weblogs.java.net/blog/pat/archive/2004/10/stupid_scanner_1.html
+        content = contentScanner.useDelimiter("\\A").next();
+      }
+
+      // System.out.println(content);
+    } catch (MalformedURLException mue) {
+      System.err.println(mue);
+    } catch (IOException ioe) {
+      System.err.println(ioe);
+    }
+
+    Matcher matcher = QUOTE_PATTERN.matcher(content);
+    while (matcher.find()) {
+      String group = matcher.group();
+
+      String symbol = null;
+      String dprice = null;
+      String change = null;
+
+      Matcher dataMatcher = DATA_PATTERN.matcher(group);
+      while (dataMatcher.find()) {
+        String tag = dataMatcher.group(1);
+        String data = dataMatcher.group(2);
+        if (tag.equals("t")) {
+          symbol = data;
+        } else if (tag.equals("l_cur")) {
+          dprice = data;
+        } else if (tag.equals("c")) {
+          change = data;
+        }
+      }
+
+      if (symbol != null && dprice != null && change != null) {
+        try {
+          int price = (int) (Double.parseDouble(dprice) * 100);
+
+          // Cache the quote (will be good for 5 seconds)
+          quotes.put(symbol, new StockServiceImpl.Quote(price, change));
+        } catch (NumberFormatException e) {
+ System.out.println("Bad price " + dprice + " for symbol " + symbol);
+        }
+      }
+    }
+  }
+
+}
=======================================
--- /trunk/bikeshed/.classpath  Wed Mar 10 14:02:29 2010
+++ /trunk/bikeshed/.classpath  Thu Mar 11 09:45:33 2010
@@ -5,7 +5,7 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/> <classpathentry kind="var" path="GWT_TOOLS/redist/json/r2_20080312/json.jar" sourcepath="/GWT_TOOLS/redist/json/r2_20080312/json-src.jar"/> - <classpathentry kind="con" path="com.google.gwt.eclipse.core.GWT_CONTAINER"/>
        <classpathentry kind="lib" path="war/WEB-INF/lib/gwt-servlet.jar"/>
+ <classpathentry kind="con" path="com.google.gwt.eclipse.core.GWT_CONTAINER/Local"/>
        <classpathentry kind="output" path="war/WEB-INF/classes"/>
 </classpath>
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/cells/client/EllipsisCell.java Thu Mar 11 08:39:56 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/cells/client/EllipsisCell.java Thu Mar 11 09:45:33 2010
@@ -15,6 +15,9 @@
  */
 package com.google.gwt.bikeshed.cells.client;

+/**
+ * Call that displays overflow using an ellipsis.
+ */
 public class EllipsisCell extends Cell<String> {

   @Override
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/StockSample.gwt.xml Thu Mar 11 08:39:56 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/StockSample.gwt.xml Thu Mar 11 09:45:33 2010
@@ -3,6 +3,7 @@
 <module rename-to='stocks'>
   <!-- Inherit the core Web Toolkit stuff.                        -->
   <inherits name='com.google.gwt.user.User'/>
+  <inherits name='com.google.gwt.regexp.RegExp'/>
   <inherits name='com.google.gwt.bikeshed.list.List'/>
   <inherits name='com.google.gwt.bikeshed.tree.Tree'/>

=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/Columns.java Thu Mar 11 08:39:56 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/Columns.java Thu Mar 11 09:45:33 2010
@@ -18,7 +18,6 @@
 import com.google.gwt.bikeshed.cells.client.ButtonCell;
 import com.google.gwt.bikeshed.cells.client.CheckboxCell;
 import com.google.gwt.bikeshed.cells.client.CurrencyCell;
-import com.google.gwt.bikeshed.cells.client.EllipsisCell;
 import com.google.gwt.bikeshed.cells.client.ProfitLossCell;
 import com.google.gwt.bikeshed.cells.client.TextCell;
 import com.google.gwt.bikeshed.list.client.Column;
@@ -62,8 +61,11 @@
     }
   };

+  // TODO - use an ellipsis cell
+  static HighlightingTextCell nameCell = new HighlightingTextCell();
+
   static Column<StockQuote, String> nameColumn =
-    new Column<StockQuote, String>(new EllipsisCell()) {
+    new Column<StockQuote, String>(nameCell) {
     @Override
     protected String getValue(StockQuote object) {
       return object.getName();
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/FavoritesWidget.java Thu Mar 11 08:39:56 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/FavoritesWidget.java Thu Mar 11 09:45:33 2010
@@ -26,6 +26,9 @@
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.Widget;

+/**
+ * Widget for favorite stocks.
+ */
 public class FavoritesWidget extends Composite {

   interface Binder extends UiBinder<Widget, FavoritesWidget> { }
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java Thu Mar 11 08:39:56 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java Thu Mar 11 09:45:33 2010
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- *
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
  * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -37,8 +37,8 @@
   interface Binder extends UiBinder<Widget, StockQueryWidget> { }
   private static final Binder binder = GWT.create(Binder.class);

-  @UiField TextBox queryField = new TextBox();
   @UiField PagingTableListView<StockQuote> listView;
+  @UiField TextBox queryField = new TextBox();

   private final ListModel<StockQuote> model;

@@ -61,17 +61,28 @@
     // Add a handler to send the name to the server
     queryField.addKeyUpHandler(new KeyUpHandler() {
       public void onKeyUp(KeyUpEvent event) {
+        Columns.nameCell.setHighlightRegex(getSearchQuery());
         updater.update();
       }
     });
   }

   public String getSearchQuery() {
-    return queryField.getText();
-  }
-
+    return normalize(queryField.getText());
+  }
+
   @UiFactory
   PagingTableListView<StockQuote> createListView() {
     return new PagingTableListView<StockQuote>(model, 10);
   }
-}
+
+  private String normalize(String input) {
+    String output = input;
+    output = output.replaceAll("\\|+", "|");
+    output = output.replaceAll("^[\\| ]+", "");
+    output = output.replaceAll("[\\| ]+$", "");
+    output = output.replaceAll("[ ]+", "|");
+ System.out.println("Replaced \"" + input + "\" with \"" + output + "\"");
+    return output;
+  }
+}
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java Thu Mar 11 08:39:56 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java Thu Mar 11 09:45:33 2010
@@ -53,6 +53,10 @@
  */
 public class StockSample implements EntryPoint, Updater {

+  interface Binder extends UiBinder<Widget, StockSample> { }
+
+  private static final Binder binder = GWT.create(Binder.class);
+
   /**
    * The delay between updates in milliseconds.
    */
@@ -61,24 +65,10 @@
   static String getFormattedPrice(int price) {
     return NumberFormat.getCurrencyFormat("USD").format(price / 100.0);
   }
-
- private final StockServiceAsync dataService = GWT.create(StockService.class);
-
- private Map<String, ListListModel<Transaction>> transactionListListModelsByTicker =
-    new HashMap<String, ListListModel<Transaction>>();
-  private List<Transaction> transactions;
-
-  private AsyncListModel<StockQuote> favoritesListModel;
-  private AsyncListModel<StockQuote> searchListModel;
-  private ListListModel<Transaction> transactionListModel;
-  private TransactionTreeViewModel treeModel;
-
-  interface Binder extends UiBinder<Widget, StockSample> { }
-  private static final Binder binder = GWT.create(Binder.class);
-
   @UiField Label cashLabel;
-  @UiField Label netWorthLabel;
+
   @UiField FavoritesWidget favoritesWidget;
+  @UiField Label netWorthLabel;
   @UiField StockQueryWidget queryWidget;
   @UiField SideBySideTreeView transactionTree;

@@ -86,6 +76,16 @@
    * The popup used to purchase stock.
    */
   private BuySellPopup buySellPopup = new BuySellPopup();
+ private final StockServiceAsync dataService = GWT.create(StockService.class);
+
+  private AsyncListModel<StockQuote> favoritesListModel;
+  private AsyncListModel<StockQuote> searchListModel;
+ private Map<String, ListListModel<Transaction>> transactionListListModelsByTicker =
+    new HashMap<String, ListListModel<Transaction>>();
+  private ListListModel<Transaction> transactionListModel;
+  private List<Transaction> transactions;
+
+  private TransactionTreeViewModel treeModel;

   /**
    * The timer used to update the stock quotes.
@@ -159,38 +159,31 @@
     update();
   }

-  public void transact(Transaction t) {
-    dataService.transact(t, new AsyncCallback<Transaction>() {
-      public void onFailure(Throwable caught) {
-        Window.alert("Error: " + caught.getMessage());
-      }
-
-      public void onSuccess(Transaction result) {
-        recordTransaction(result);
-        update();
-      }
-
-      /**
-       * Update transactions (list of all transactions),
-       * transactionTickers (set of all tickers involved in
-       * transactions), and transactionsByTicker (map from
-       * ticker to lists of transactions for that ticker).
-       */
-      private void recordTransaction(Transaction result) {
-        transactions.add(0, result);
-        String ticker = result.getTicker();
-
-        // Update the next level of the transaction tree
-        // for the given ticker
-        ListListModel<Transaction> t =
-          transactionListListModelsByTicker.get(ticker);
-        if (t == null) {
-          t = new ListListModel<Transaction>();
-          transactionListListModelsByTicker.put(ticker, t);
-        }
-        t.getList().add(result);
-      }
-    });
+  /**
+   * Process the {...@link StockResponse} from the server.
+   *
+   * @param response the stock response
+   */
+  public void processStockResponse(StockResponse response) {
+    // Update the search list.
+    StockQuoteList searchResults = response.getSearchResults();
+    searchListModel.updateDataSize(response.getNumSearchResults(), true);
+    searchListModel.updateViewData(searchResults.getStartIndex(),
+        searchResults.size(), searchResults);
+
+    // Update the favorites list.
+    updateFavorites(response);
+    updateSector(response);
+
+    // Update available cash.
+    int cash = response.getCash();
+    int netWorth = response.getNetWorth();
+    cashLabel.setText(getFormattedPrice(cash));
+    netWorthLabel.setText(getFormattedPrice(netWorth));
+    buySellPopup.setAvailableCash(cash);
+
+    // Restart the update timer.
+    updateTimer.schedule(UPDATE_DELAY);
   }

   /**
@@ -225,6 +218,40 @@
     }
   }

+  public void transact(Transaction t) {
+    dataService.transact(t, new AsyncCallback<Transaction>() {
+      public void onFailure(Throwable caught) {
+        Window.alert("Error: " + caught.getMessage());
+      }
+
+      public void onSuccess(Transaction result) {
+        recordTransaction(result);
+        update();
+      }
+
+      /**
+       * Update transactions (list of all transactions),
+       * transactionTickers (set of all tickers involved in
+       * transactions), and transactionsByTicker (map from
+       * ticker to lists of transactions for that ticker).
+       */
+      private void recordTransaction(Transaction result) {
+        transactions.add(0, result);
+        String ticker = result.getTicker();
+
+        // Update the next level of the transaction tree
+        // for the given ticker
+        ListListModel<Transaction> t =
+          transactionListListModelsByTicker.get(ticker);
+        if (t == null) {
+          t = new ListListModel<Transaction>();
+          transactionListListModelsByTicker.put(ticker, t);
+        }
+        t.getList().add(result);
+      }
+    });
+  }
+
   /**
    * Request data from the server using the last query string.
    */
@@ -248,6 +275,7 @@
     }

     String searchQuery = queryWidget.getSearchQuery();
+
     StockRequest request = new StockRequest(searchQuery,
         sectorListModel != null ? sectorListModel.getSector() : null,
         searchRanges[0],
@@ -270,46 +298,6 @@
       }
     });
   }
-
-  // Hack - walk the transaction tree to find the current viewed sector
-  private String getSectorName() {
-    int children = transactionTree.getRootNode().getChildCount();
-    for (int i = 0; i < children; i++) {
- TreeNode<?> childNode = transactionTree.getRootNode().getChildNode(i);
-      if (childNode.isOpen()) {
-        return (String) childNode.getValue();
-      }
-    }
-
-    return null;
-  }
-
-  /**
-   * Process the {...@link StockResponse} from the server.
-   *
-   * @param response the stock response
-   */
-  public void processStockResponse(StockResponse response) {
-    // Update the search list.
-    StockQuoteList searchResults = response.getSearchResults();
-    searchListModel.updateDataSize(response.getNumSearchResults(), true);
-    searchListModel.updateViewData(searchResults.getStartIndex(),
-        searchResults.size(), searchResults);
-
-    // Update the favorites list.
-    updateFavorites(response);
-    updateSector(response);
-
-    // Update available cash.
-    int cash = response.getCash();
-    int netWorth = response.getNetWorth();
-    cashLabel.setText(getFormattedPrice(cash));
-    netWorthLabel.setText(getFormattedPrice(netWorth));
-    buySellPopup.setAvailableCash(cash);
-
-    // Restart the update timer.
-    updateTimer.schedule(UPDATE_DELAY);
-  }

   public void updateFavorites(StockResponse response) {
     // Update the favorites list.
@@ -324,9 +312,11 @@
     StockQuoteList sectorList = response.getSector();
     if (sectorList != null) {
SectorListModel sectorListModel = treeModel.getSectorListModel(getSectorName());
-      sectorListModel.updateDataSize(response.getNumSector(), true);
-      sectorListModel.updateViewData(sectorList.getStartIndex(),
-          sectorList.size(), sectorList);
+      if (sectorListModel != null) {
+        sectorListModel.updateDataSize(response.getNumSector(), true);
+        sectorListModel.updateViewData(sectorList.getStartIndex(),
+            sectorList.size(), sectorList);
+      }
     }
   }

@@ -344,4 +334,17 @@
   SideBySideTreeView createTransactionTree() {
     return new SideBySideTreeView(treeModel, null, 200, 200);
   }
-}
+
+  // Hack - walk the transaction tree to find the current viewed sector
+  private String getSectorName() {
+    int children = transactionTree.getRootNode().getChildCount();
+    for (int i = 0; i < children; i++) {
+ TreeNode<?> childNode = transactionTree.getRootNode().getChildNode(i);
+      if (childNode.isOpen()) {
+        return (String) childNode.getValue();
+      }
+    }
+
+    return null;
+  }
+}
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/common.css Thu Mar 11 08:39:56 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/common.css Thu Mar 11 09:45:33 2010
@@ -5,7 +5,7 @@
   border-right: 12px solid white;
   border-bottom: 14px solid white;
   -webkit-border-image: url(border.png) 8 12 14 8 round round;
-  -gecko-border-image: url(border.png) 8 12 14 8 round round;
+  -moz-border-image: url(border.png) 8 12 14 8 round round;
 }

 .header {
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/StockServiceImpl.java Thu Mar 11 03:44:25 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/StockServiceImpl.java Thu Mar 11 09:45:33 2010
@@ -25,17 +25,11 @@
 import com.google.gwt.bikeshed.sample.stocks.shared.Transaction;
 import com.google.gwt.user.server.rpc.RemoteServiceServlet;

-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
+import java.util.Locale;
 import java.util.Map;
-import java.util.Scanner;
 import java.util.Set;
+import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -48,6 +42,30 @@
 public class StockServiceImpl extends RemoteServiceServlet implements
     StockService {

+  static class Quote {
+    String change;
+    long createdTime;
+    int price;
+
+    public Quote(int price, String change) {
+      this.price = price;
+      this.change = change;
+      this.createdTime = System.currentTimeMillis();
+    }
+
+    public String getChange() {
+      return change;
+    }
+
+    public long getCreatedTime() {
+      return createdTime;
+    }
+
+    public int getPrice() {
+      return price;
+    }
+  }
+
   /**
* The result of a query to the remote service that provides stock quotes.
    */
@@ -60,33 +78,16 @@
       this.numRows = numRows;
     }
   }
-
- static HashMap<String, String> companyNamesBySymbol = new HashMap<String, String>();
-
-  static TreeSet<String> stockTickers = new TreeSet<String>();
-
- private static final Pattern DATA_PATTERN = Pattern.compile("\"([^\"]*)\"\\s*:\\s*\"([^\"]*)\"");

   private static final int MAX_RESULTS_TO_RETURN = 10000;

- private static final Pattern QUOTE_PATTERN = Pattern.compile("\\{[^\\}]*\\}"); + private static final Map<String, Quote> QUOTES = new HashMap<String, Quote>();

   private static final HashMap<String,Pattern> sectorPatterns =
     new HashMap<String,Pattern>();

   private static final HashMap<String,String> sectorQueries =
     new HashMap<String,String>();
-
-  static {
-    int num = Stocks.SYMBOLS.size();
-    for (int i = 0; i < num - 1; i += 2) {
-      String symbol = Stocks.SYMBOLS.get(i);
-      String companyName = Stocks.SYMBOLS.get(i + 1);
-      stockTickers.add(symbol);
-
-      companyNamesBySymbol.put(symbol, companyName);
-    }
-  }

   static {
     sectorQueries.put("DOW JONES INDUSTRIALS",
@@ -148,8 +149,7 @@
   public StockResponse addFavorite(String ticker, Range favoritesRange) {
     PlayerStatus player = ensurePlayer();
     player.addFavorite(ticker);
-    Result favorites = query(player.getFavoritesQuery(),
-        player.getFavoritesPattern(), favoritesRange, false);
+    Result favorites = queryFavorites(favoritesRange);
     return new StockResponse(null, favorites.quotes, null, null,
         0, favorites.numRows, 0, player.getCash());
   }
@@ -161,7 +161,7 @@
       return null;
     }
     Pattern sectorPattern = sectorPatterns.get(sector);
-    return query(sectorQuery, sectorPattern, sectorRange, false);
+    return queryTickerRegex(sectorPattern, sectorRange);
   }

   public StockResponse getStockQuotes(StockRequest request)
@@ -176,9 +176,8 @@
     Range sectorRange = request.getSectorRange();

     PlayerStatus player = ensurePlayer();
-    Result searchResults = query(query, compile(query), searchRange, true);
-    Result favorites = query(player.getFavoritesQuery(),
-        player.getFavoritesPattern(), favoritesRange, false);
+    Result searchResults = getSearchQuotes(query, searchRange);
+    Result favorites = queryFavorites(favoritesRange);
     String sectorName = request.getSector();
     Result sector = sectorRange != null ?
         getSectorQuotes(sectorName, sectorRange) : null;
@@ -192,11 +191,11 @@
         sector != null ? sector.numRows : 0,
         player.getCash());
   }
-
+
public StockResponse removeFavorite(String ticker, Range favoritesRange) {
     PlayerStatus player = ensurePlayer();
     player.removeFavorite(ticker);
- Result favorites = query(player.getFavoritesQuery(), player.getFavoritesPattern(), favoritesRange, false);
+    Result favorites = queryFavorites(favoritesRange);
     return new StockResponse(null, favorites.quotes, null, null,
         0, favorites.numRows, 0, player.getCash());
   }
@@ -209,15 +208,7 @@
       throw new IllegalArgumentException("Stock could not be found");
     }

-    String tickerRegex = ticker;
-    if (!ticker.startsWith("^")) {
-      tickerRegex = "^" + tickerRegex;
-    }
-    if (!ticker.endsWith("$")) {
-      tickerRegex = tickerRegex + "$";
-    }
-    Pattern tickerPattern = compile(ticker);
- Result result = query(tickerRegex, tickerPattern, new DefaultRange(0, 1), false);
+    Result result = queryExactTicker(ticker);
     if (result.numRows != 1 || result.quotes.size() != 1) {
       throw new IllegalArgumentException("Could not resolve stock ticker");
     }
@@ -251,158 +242,185 @@
     return player;
   }

- private List<String> getTickers(String query, Pattern pattern, boolean matchNames) {
-    Set<String> tickers = new TreeSet<String>();
-    if (query.length() > 0) {
-      query = query.toUpperCase();
-
-      int count = 0;
-      for (String ticker : stockTickers) {
- if (ticker.startsWith(query) || (pattern != null && match(ticker, pattern))) {
-          tickers.add(ticker);
-          count++;
-          if (count > MAX_RESULTS_TO_RETURN) {
-            break;
-          }
-        }
-      }
-
-      if (matchNames && pattern != null) {
- for (Map.Entry<String,String> entry : companyNamesBySymbol.entrySet()) {
-          if (match(entry.getValue(), pattern)) {
-            tickers.add(entry.getKey());
-            count++;
-            if (count > MAX_RESULTS_TO_RETURN) {
-              break;
-            }
-          }
-        }
+  private Result getQuotes(SortedSet<String> symbols, Range range) {
+    int start = range.getStart();
+    int end = Math.min(start + range.getLength(), symbols.size());
+
+    if (end <= start) {
+      return new Result(new StockQuoteList(0), 0);
+    }
+
+    // Get the symbols that are in range.
+    SortedSet<String> symbolsInRange = new TreeSet<String>();
+    int idx = 0;
+    for (String symbol : symbols) {
+      if (idx >= start && idx < end) {
+        symbolsInRange.add(symbol);
+      }
+      idx++;
+    }
+
+    // If we already have a price that is less than 5 seconds old,
+    // don't re-request the data from the server
+
+    SortedSet<String> symbolsToQuery = new TreeSet<String>();
+    long now = System.currentTimeMillis();
+    for (String symbol : symbolsInRange) {
+      Quote quote = QUOTES.get(symbol);
+      if (quote == null || now - quote.getCreatedTime() >= 5000) {
+        symbolsToQuery.add(symbol);
+        // System.out.println("retrieving new value of " + symbol);
+      } else {
+ // System.out.println("Using cached value of " + symbol + " (" + (now - quote.getCreatedTime()) + "ms old)");
       }
     }
-
-    return new ArrayList<String>(tickers);
+
+    if (symbolsToQuery.size() > 0) {
+      GoogleFinance.queryServer(symbolsToQuery, QUOTES);
+    }
+
+    // Create and return a StockQuoteList containing the quotes
+    StockQuoteList toRet = new StockQuoteList(start);
+    for (String symbol : symbolsInRange) {
+      Quote quote = QUOTES.get(symbol);
+
+      if (quote == null) {
+        System.out.println("Bad symbol " + symbol);
+      } else {
+        String name = Stocks.companyNamesBySymbol.get(symbol);
+        PlayerStatus player = ensurePlayer();
+        Integer sharesOwned = player.getSharesOwned(symbol);
+        boolean favorite = player.isFavorite(symbol);
+        int totalPaid = player.getAverageCostBasis(symbol);
+
+        toRet.add(new StockQuote(symbol, name, quote.getPrice(),
+ quote.getChange(), sharesOwned == null ? 0 : sharesOwned.intValue(),
+                favorite, totalPaid));
+      }
+    }
+
+    return new Result(toRet, symbols.size());
   }

-  private boolean match(String symbol, Pattern pattern) {
-    Matcher m = pattern.matcher(symbol);
-    return m.matches();
-  }
-
-  /**
-   * Query the remote service to retrieve current stock prices.
-   *
-   * @param query the query string
-   * @param range the range of results requested
-   * @return the stock quotes
-   */
-  private Result query(String query, Pattern queryPattern, Range range,
-      boolean matchNames) {
-    // Get all symbols for the query.
-    PlayerStatus player = ensurePlayer();
-    List<String> symbols = getTickers(query, queryPattern, matchNames);
-
-    if (symbols.size() == 0) {
-      return new Result(new StockQuoteList(0), 0);
+  // If a query is alpha-only ([A-Za-z]+), return stocks for which:
+  //   1a) a prefix of the ticker symbol matches the query
+  //   2) any substring of the stock name matches the query
+  //
+  // If a query is non-alpha, consider it as a regex and return stocks for
+  // which:
+  //   1b) any portion of the stock symbol matches the regex
+  //   2) any portion of the stock name matches the regex
+  private Result getSearchQuotes(String query, Range searchRange) {
+    SortedSet<String> symbols = new TreeSet<String>();
+
+    boolean queryIsAlpha = true;
+    for (int i = 0; i < query.length(); i++) {
+      char c = query.charAt(i);
+      if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) {
+        queryIsAlpha = false;
+        break;
+      }
+    }
+
+    // Canonicalize case
+    query = query.toUpperCase(Locale.US);
+
+    // (1a)
+    if (queryIsAlpha) {
+      getTickersByPrefix(query, symbols);
+    }
+
+    // Use Unicode case-insensitive matching, allow matching of a substring
+    Pattern pattern = compile("(?iu).*(" + query + ").*");
+    if (pattern != null) {
+      // (1b)
+      if (!queryIsAlpha) {
+        getTickersBySymbolRegex(pattern, symbols);
+      }
+
+      // (2)
+      getTickersByNameRegex(pattern, symbols);
     }

-    int start = range.getStart();
-    int end = Math.min(start + range.getLength(), symbols.size());
-
-    // Get the symbols that are in range.
-    Set<String> symbolsInRange = new HashSet<String>();
-    if (end > start) {
-      symbolsInRange.addAll(symbols.subList(start, end));
-    }
-
-    // Build the URL string.
-    StringBuilder sb = new StringBuilder(
-        "http://www.google.com/finance/info?client=ig&q=";);
-    boolean first = true;
-    for (String symbol : symbolsInRange) {
-      if (!first) {
-        sb.append(',');
-      }
-      sb.append(symbol);
-      first = false;
-    }
-
-    if (first) {
-      // No symbols
-      return new Result(new StockQuoteList(0), 0);
-    }
-
-    // Send the request.
-    String content = "";
-    try {
-      String urlString = sb.toString();
-      URL url = new URL(urlString);
-      InputStream urlInputStream = url.openStream();
-      Scanner contentScanner = new Scanner(urlInputStream, "UTF-8");
-      if (contentScanner.hasNextLine()) {
-        // See
- // http://weblogs.java.net/blog/pat/archive/2004/10/stupid_scanner_1.html
-        content = contentScanner.useDelimiter("\\A").next();
-      }
-
-      // System.out.println(content);
-    } catch (MalformedURLException mue) {
-      System.err.println(mue);
-    } catch (IOException ioe) {
-      System.err.println(ioe);
-    }
-
-    // Parse response.
-    Map<String, StockQuote> priceMap = new HashMap<String, StockQuote>();
-    Matcher matcher = QUOTE_PATTERN.matcher(content);
-    while (matcher.find()) {
-      String group = matcher.group();
-
-      String symbol = null;
-      String price = null;
-      String change = null;
-
-      Matcher dataMatcher = DATA_PATTERN.matcher(group);
-      while (dataMatcher.find()) {
-        String tag = dataMatcher.group(1);
-        String data = dataMatcher.group(2);
-        if (tag.equals("t")) {
-          symbol = data;
-        } else if (tag.equals("l_cur")) {
-          price = data;
-        } else if (tag.equals("c")) {
-          change = data;
-        }
-      }
-
-      if (symbol != null && price != null) {
-        int iprice = 0;
-        try {
-          iprice = (int) (Double.parseDouble(price) * 100);
-          String name = companyNamesBySymbol.get(symbol);
-          Integer sharesOwned = player.getSharesOwned(symbol);
-          boolean favorite = player.isFavorite(symbol);
-          int totalPaid = player.getAverageCostBasis(symbol);
-          priceMap.put(symbol, new StockQuote(symbol, name, iprice, change,
-              sharesOwned == null ? 0 : sharesOwned.intValue(), favorite,
-                  totalPaid));
-        } catch (NumberFormatException e) {
- System.out.println("Bad price " + price + " for symbol " + symbol);
-        }
+    return getQuotes(symbols, searchRange);
+  }
+
+  // Assume pattern is upper case
+ private void getTickersByNameRegex(Pattern pattern, Set<String> tickers) {
+    if (pattern == null) {
+      return;
+    }
+
+ for (Map.Entry<String,String> entry : Stocks.companyNamesBySymbol.entrySet()) {
+      if (tickers.size() >= MAX_RESULTS_TO_RETURN) {
+        return;
+      }
+
+      if (match(entry.getValue(), pattern)) {
+        tickers.add(entry.getKey());
+      }
+    }
+  }
+
+  // Assume prefix is upper case
+  private void getTickersByPrefix(String prefix, Set<String> tickers) {
+    if (prefix == null || prefix.length() == 0) {
+      return;
+    }
+
+    for (String ticker : Stocks.stockTickers) {
+      if (tickers.size() >= MAX_RESULTS_TO_RETURN) {
+        break;
+      }
+
+      if (ticker.startsWith(prefix)) {
+        tickers.add(ticker);
+      }
+    }
+  }
+
+  // Assume pattern is upper case
+ private void getTickersBySymbolRegex(Pattern pattern, Set<String> tickers) {
+    if (pattern == null) {
+      return;
+    }
+
+    for (String ticker : Stocks.stockTickers) {
+      if (tickers.size() >= MAX_RESULTS_TO_RETURN) {
+        return;
+      }
+      if (match(ticker, pattern)) {
+        tickers.add(ticker);
       }
     }
-
-    // Convert the price map to a StockQuoteList.
-    StockQuoteList toRet = new StockQuoteList(start);
-    for (int i = start; i < end; i++) {
-      String symbol = symbols.get(i);
-      StockQuote quote = priceMap.get(symbol);
-      if (quote == null) {
-        System.out.println("Bad symbol " + symbol);
-      } else {
-        toRet.add(quote);
-      }
-    }
-
-    return new Result(toRet, symbols.size());
+  }
+
+  private boolean match(String symbol, Pattern pattern) {
+    Matcher m = pattern.matcher(symbol);
+    return m.matches();
+  }
+
+  private Result queryExactTicker(String ticker) {
+    SortedSet<String> symbols = new TreeSet<String>();
+    symbols.add(ticker);
+    return getQuotes(symbols, new DefaultRange(0, 1));
+  }
+
+  private Result queryFavorites(Range favoritesRange) {
+    PlayerStatus player = ensurePlayer();
+    SortedSet<String> symbols = new TreeSet<String>();
+
+    Pattern favoritesPattern = player.getFavoritesPattern();
+    if (favoritesPattern != null) {
+      getTickersBySymbolRegex(favoritesPattern, symbols);
+    }
+
+    return getQuotes(symbols, favoritesRange);
+  }
+
+  private Result queryTickerRegex(Pattern pattern, Range range) {
+    SortedSet<String> symbols = new TreeSet<String>();
+    getTickersBySymbolRegex(pattern, symbols);
+    return getQuotes(symbols, range);
   }
 }
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/Stocks.java Wed Mar 10 08:08:46 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/Stocks.java Thu Mar 11 09:45:33 2010
@@ -16,7 +16,9 @@
 package com.google.gwt.bikeshed.sample.stocks.server;

 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.TreeSet;

 /**
  * A list of NYSE and NASDAQ stocks (note: this is a snapshot plus some
@@ -26,6 +28,11 @@

   public static final List<String> SYMBOLS = new ArrayList<String>();

+  public static final HashMap<String, String> companyNamesBySymbol =
+    new HashMap<String, String>();
+
+  public static final TreeSet<String> stockTickers = new TreeSet<String>();
+
   static {
     s("A", "Agilent Technologies Inc.");
     s("AA", "Alcoa Inc.");
@@ -6130,8 +6137,15 @@
     s("ZUMZ", "Zumiez Inc.");
     s("ZZ", "Sealy Corporation");
     s("ZZC", "SEALY CORPORATION");
-  };
-
+
+    int num = SYMBOLS.size();
+    for (int i = 0; i < num - 1; i += 2) {
+      String symbol = SYMBOLS.get(i);
+      String companyName = SYMBOLS.get(i + 1);
+      stockTickers.add(symbol);
+      companyNamesBySymbol.put(symbol, companyName);
+    }
+  }
   private static void s(String symbol, String name) {
     SYMBOLS.add(symbol);
     SYMBOLS.add(name);
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockQuote.java Thu Mar 11 03:44:25 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockQuote.java Thu Mar 11 09:45:33 2010
@@ -24,7 +24,6 @@

   private boolean favorite;
   private String name;
-  private transient String notes;
   private int price;
   private String change;
   private String ticker;
@@ -79,10 +78,6 @@
   public String getName() {
     return name;
   }
-
-  public String getNotes() {
-    return notes;
-  }

   public int getPrice() {
     return price;
@@ -107,15 +102,11 @@
   public boolean isFavorite() {
     return favorite;
   }
-
-  public void setNotes(String notes) {
-    this.notes = notes;
-  }

   @Override
   public String toString() {
return "StockQuote [ticker=" + ticker + ", name=\"" + name + "\", price="
-        + price + ", notes=\"" + notes + "\", favorite=" + favorite
+        + price + ", favorite=" + favorite
         + ", totalPaid=" + totalPaid + "]";
   }
 }
=======================================
--- /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockRequest.java Wed Mar 10 05:41:10 2010 +++ /trunk/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockRequest.java Thu Mar 11 09:45:33 2010
@@ -29,6 +29,7 @@
   Range searchRange;
   String sector;
   Range sectorRange;
+
   public StockRequest(String searchQuery, String sector, Range searchRange,
       Range favoritesRange, Range sectorRange) {
     this.searchQuery = searchQuery;
=======================================
--- /trunk/bikeshed/src/com/google/gwt/sample/expenses/domain/RelationshipValidationVisitor.java Wed Mar 10 14:02:29 2010 +++ /trunk/bikeshed/src/com/google/gwt/sample/expenses/domain/RelationshipValidationVisitor.java Thu Mar 11 09:45:33 2010
@@ -17,9 +17,10 @@

 /**
* Used by {...@link Storage#persist(Entity)} to ensure relationships are valid
- * (can't point to an Entity with no id);
+ * (can't point to an Entity with no id).
  */
 public class RelationshipValidationVisitor implements EntityVisitor<Void> {
+
   public Void visit(Currency currency) {
     return null;
   }
@@ -50,5 +51,4 @@
           to));
     }
   }
-
-}
+}
=======================================
--- /trunk/bikeshed/src/com/google/gwt/sample/expenses/shared/EmployeeRequests.java Wed Mar 10 14:02:29 2010 +++ /trunk/bikeshed/src/com/google/gwt/sample/expenses/shared/EmployeeRequests.java Thu Mar 11 09:45:33 2010
@@ -32,8 +32,7 @@
 import java.util.List;

 /**
- * "Generated" from static methods of
- * {...@link com.google.gwt.sample.expenses.domain.Employee}.
+ * "Generated" from static methods of {...@link com.google.gwt.sample.expenses.domain.Employee}.
  */
 public class EmployeeRequests {

=======================================
--- /trunk/bikeshed/src/com/google/gwt/sample/expenses/shared/ReportRequests.java Wed Mar 10 14:02:29 2010 +++ /trunk/bikeshed/src/com/google/gwt/sample/expenses/shared/ReportRequests.java Thu Mar 11 09:45:33 2010
@@ -36,7 +36,7 @@

 /**
  * "Generated" from static methods of
- * {...@link com.google.gwt.sample.expenses.domain.Employee}
+ * {...@link com.google.gwt.sample.expenses.domain.Employee}.
  */
 public class ReportRequests {

=======================================
--- /trunk/bikeshed/test/com/google/gwt/sample/expenses/domain/StorageTest.java Wed Mar 10 14:02:29 2010 +++ /trunk/bikeshed/test/com/google/gwt/sample/expenses/domain/StorageTest.java Thu Mar 11 09:45:33 2010
@@ -25,29 +25,10 @@
 public class StorageTest extends TestCase {
   Storage store = new Storage();

-  public void testReportsByEmployeeIndex() {
-    Storage s = new Storage();
-    Storage.fill(s);
-
-    Employee abc = s.findEmployeeByUserName("abc");
-    List<Report> reports = s.findReportsByEmployee(abc.getId());
-    assertEquals(3, reports.size());
-
-    Report report = new Report();
-    report.setReporter(abc);
-    report = s.persist(report);
-
-    reports = s.findReportsByEmployee(abc.getId());
-    assertEquals(4, reports.size());
-    Report latestReport = reports.get(3);
-    assertEquals(report.getId(), latestReport.getId());
-    assertEquals(report.getVersion(), latestReport.getVersion());
-  }
-
   public void testFreshRelationships() {
     Storage s = new Storage();
     Storage.fill(s);
-
+
     Employee abc = s.findEmployeeByUserName("abc");
     List<Report> reports = s.findReportsByEmployee(abc.getId());
     for (Report report : reports) {
@@ -66,6 +47,25 @@
     }
   }

+  public void testReportsByEmployeeIndex() {
+    Storage s = new Storage();
+    Storage.fill(s);
+
+    Employee abc = s.findEmployeeByUserName("abc");
+    List<Report> reports = s.findReportsByEmployee(abc.getId());
+    assertEquals(3, reports.size());
+
+    Report report = new Report();
+    report.setReporter(abc);
+    report = s.persist(report);
+
+    reports = s.findReportsByEmployee(abc.getId());
+    assertEquals(4, reports.size());
+    Report latestReport = reports.get(3);
+    assertEquals(report.getId(), latestReport.getId());
+    assertEquals(report.getVersion(), latestReport.getVersion());
+  }
+
   public void testUserNameIndex() {
     Storage s = new Storage();
     Storage.fill(s);
@@ -120,6 +120,14 @@
     assertEquals(v2.getVersion(), anotherV2.getVersion());
   }

+  private Entity doTestNew(Entity e) {
+    Entity v1 = store.persist(e);
+    assertEquals(Integer.valueOf(0), v1.getVersion());
+    assertNotNull(v1.getId());
+    assertNotSame(v1, store.get(Storage.startSparseEdit(v1)));
+    return v1;
+  }
+
   private Entity doTestSparseEdit(Entity v1) {
     Entity delta = Storage.startSparseEdit(v1);
     Entity v2 = store.persist(delta);
@@ -131,12 +139,4 @@
     assertEquals(v2.getVersion(), anotherV2.getVersion());
     return anotherV2;
   }
-
-  private Entity doTestNew(Entity e) {
-    Entity v1 = store.persist(e);
-    assertEquals(Integer.valueOf(0), v1.getVersion());
-    assertNotNull(v1.getId());
-    assertNotSame(v1, store.get(Storage.startSparseEdit(v1)));
-    return v1;
-  }
-}
+}
=======================================
--- /trunk/bikeshed/war/Stocks.css      Thu Mar 11 08:39:56 2010
+++ /trunk/bikeshed/war/Stocks.css      Thu Mar 11 09:45:33 2010
@@ -30,7 +30,7 @@
 }

 div.gwt-sstree-oddRow {
-    background-color: rgb(220, 220, 220);
+  background-color: rgb(220, 220, 220);
 }

 /** Example rules used by the template application (remove for your app) */
@@ -70,7 +70,7 @@
   border-right: 11px solid white;
   border-bottom: 11px solid white;
   -webkit-border-image: url(blueborder.png) 8 11 11 8 round round;
-  -gecko-border-image: url(blueborder.png) 8 11 11 8 round round;
+  -moz-border-image: url(blueborder.png) 8 11 11 8 round round;
 }

 .gwt-DialogBox .Caption {

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to