Reduce requests to finance server
Highlight search terms in stock names
Clean up some regex handling
Fix checkstyle problems
Merge with Joel's changes


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7706 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/.classpath b/bikeshed/.classpath
index 4d52a95..1031fe8 100644
--- a/bikeshed/.classpath
+++ b/bikeshed/.classpath
@@ -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>
diff --git a/bikeshed/src/com/google/gwt/bikeshed/cells/client/EllipsisCell.java b/bikeshed/src/com/google/gwt/bikeshed/cells/client/EllipsisCell.java
index 72e9039..8e9f4c8 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/cells/client/EllipsisCell.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/cells/client/EllipsisCell.java
@@ -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
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/StockSample.gwt.xml b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/StockSample.gwt.xml
index 8500059..5c2284b 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/StockSample.gwt.xml
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/StockSample.gwt.xml
@@ -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'/>
 
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/Columns.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/Columns.java
index 426d715..95d6ba8 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/Columns.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/Columns.java
@@ -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();
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/FavoritesWidget.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/FavoritesWidget.java
index afa4c3a..c099b4d 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/FavoritesWidget.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/FavoritesWidget.java
@@ -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> { }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/HighlightingTextCell.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/HighlightingTextCell.java
new file mode 100644
index 0000000..0ed535b
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/HighlightingTextCell.java
@@ -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;
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java
index 83a52ff..9ddfaf0 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java
@@ -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;
+  }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
index 41f7ef7..be7e403 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
@@ -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,6 +159,65 @@
     update();
   }
 
+  /**
+   * 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);
+  }
+
+  /**
+   * Set or unset a ticker symbol as a 'favorite'.
+   *
+   * @param ticker the ticker symbol
+   * @param favorite if true, make the stock a favorite
+   */
+  public void setFavorite(String ticker, boolean favorite) {
+    if (favorite) {
+      dataService.addFavorite(ticker, favoritesListModel.getRanges()[0],
+          new AsyncCallback<StockResponse>() {
+        public void onFailure(Throwable caught) {
+          Window.alert("Error adding favorite");
+        }
+
+        public void onSuccess(StockResponse response) {
+          updateFavorites(response);
+        }
+      });
+    } else {
+      dataService.removeFavorite(ticker, favoritesListModel.getRanges()[0],
+          new AsyncCallback<StockResponse>() {
+        public void onFailure(Throwable caught) {
+          Window.alert("Error removing favorite");
+        }
+
+        public void onSuccess(StockResponse response) {
+          updateFavorites(response);
+        }
+      });
+    }
+  }
+  
   public void transact(Transaction t) {
     dataService.transact(t, new AsyncCallback<Transaction>() {
       public void onFailure(Throwable caught) {
@@ -194,38 +253,6 @@
   }
 
   /**
-   * Set or unset a ticker symbol as a 'favorite'.
-   *
-   * @param ticker the ticker symbol
-   * @param favorite if true, make the stock a favorite
-   */
-  public void setFavorite(String ticker, boolean favorite) {
-    if (favorite) {
-      dataService.addFavorite(ticker, favoritesListModel.getRanges()[0],
-          new AsyncCallback<StockResponse>() {
-        public void onFailure(Throwable caught) {
-          Window.alert("Error adding favorite");
-        }
-
-        public void onSuccess(StockResponse response) {
-          updateFavorites(response);
-        }
-      });
-    } else {
-      dataService.removeFavorite(ticker, favoritesListModel.getRanges()[0],
-          new AsyncCallback<StockResponse>() {
-        public void onFailure(Throwable caught) {
-          Window.alert("Error removing favorite");
-        }
-
-        public void onSuccess(StockResponse response) {
-          updateFavorites(response);
-        }
-      });
-    }
-  }
-  
-  /**
    * Request data from the server using the last query string.
    */
   public void update() {
@@ -248,6 +275,7 @@
     }
 
     String searchQuery = queryWidget.getSearchQuery();
+    
     StockRequest request = new StockRequest(searchQuery,
         sectorListModel != null ? sectorListModel.getSector() : null,
         searchRanges[0],
@@ -271,46 +299,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.
     StockQuoteList favorites = response.getFavorites();
@@ -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;
+  }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/common.css b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/common.css
index 8ab5593..803f224 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/common.css
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/common.css
@@ -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 {
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/GoogleFinance.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/GoogleFinance.java
new file mode 100644
index 0000000..16b9c94
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/GoogleFinance.java
@@ -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);
+        }
+      }
+    }
+  }
+
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/StockServiceImpl.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/StockServiceImpl.java
index 2cac311..600565f 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/StockServiceImpl.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/StockServiceImpl.java
@@ -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.
    */
@@ -61,32 +79,15 @@
     }
   }
 
-  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;
-          }
-        }
+  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);
       }
-
-      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;
-            }
-          }
-        }
+      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());
   }
 
+  // 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);
+    }
+
+    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);
+      }
+    }
+  }
+  
   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.
+  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();
-    List<String> symbols = getTickers(query, queryPattern, matchNames);
+    SortedSet<String> symbols = new TreeSet<String>();
 
-    if (symbols.size() == 0) {
-      return new Result(new StockQuoteList(0), 0);
+    Pattern favoritesPattern = player.getFavoritesPattern();
+    if (favoritesPattern != null) {
+      getTickersBySymbolRegex(favoritesPattern, symbols);
     }
+    
+    return getQuotes(symbols, favoritesRange);
+  }
 
-    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);
-        }
-      }
-    }
-
-    // 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 Result queryTickerRegex(Pattern pattern, Range range) {
+    SortedSet<String> symbols = new TreeSet<String>();
+    getTickersBySymbolRegex(pattern, symbols);
+    return getQuotes(symbols, range);
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/Stocks.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/Stocks.java
index c37c565..61906b6 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/Stocks.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/server/Stocks.java
@@ -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);
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockQuote.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockQuote.java
index 881ba14..da9cf09 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockQuote.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockQuote.java
@@ -24,7 +24,6 @@
 
   private boolean favorite;
   private String name;
-  private transient String notes;
   private int price;
   private String change;
   private String ticker;
@@ -80,10 +79,6 @@
     return name;
   }
 
-  public String getNotes() {
-    return notes;
-  }
-
   public int getPrice() {
     return price;
   }
@@ -108,14 +103,10 @@
     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 + "]";
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockRequest.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockRequest.java
index c612728..dee4beb 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockRequest.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/shared/StockRequest.java
@@ -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;
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/domain/RelationshipValidationVisitor.java b/bikeshed/src/com/google/gwt/sample/expenses/domain/RelationshipValidationVisitor.java
index 0b34d82..4e37f5d 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/domain/RelationshipValidationVisitor.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/domain/RelationshipValidationVisitor.java
@@ -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));
     }
   }
-
 }
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/shared/EmployeeRequests.java b/bikeshed/src/com/google/gwt/sample/expenses/shared/EmployeeRequests.java
index c5dced9..8de4419 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/shared/EmployeeRequests.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/shared/EmployeeRequests.java
@@ -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 {
 
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/shared/ReportRequests.java b/bikeshed/src/com/google/gwt/sample/expenses/shared/ReportRequests.java
index e2cad7c..72a0704 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/shared/ReportRequests.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/shared/ReportRequests.java
@@ -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 {
 
diff --git a/bikeshed/test/com/google/gwt/sample/expenses/domain/StorageTest.java b/bikeshed/test/com/google/gwt/sample/expenses/domain/StorageTest.java
index 3318062..8e14009 100644
--- a/bikeshed/test/com/google/gwt/sample/expenses/domain/StorageTest.java
+++ b/bikeshed/test/com/google/gwt/sample/expenses/domain/StorageTest.java
@@ -25,6 +25,28 @@
 public class StorageTest extends TestCase {
   Storage store = new Storage();
 
+  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) {
+      assertEquals(abc.getVersion(), report.getReporter().getVersion());
+    }
+    
+    abc.setDisplayName("Herbert");
+    s.persist(abc);
+    List<Report> fresherReports = s.findReportsByEmployee(abc.getId());
+    assertEquals(reports.size(), fresherReports.size());
+    Integer expectedVersion = abc.getVersion() + 1;
+    for (Report report : fresherReports) {
+      assertEquals(abc.getId(), report.getReporter().getId());
+      assertEquals(expectedVersion, report.getReporter().getVersion());
+      assertEquals("Herbert", report.getReporter().getDisplayName());
+    }
+  }
+
   public void testReportsByEmployeeIndex() {
     Storage s = new Storage();
     Storage.fill(s);
@@ -44,28 +66,6 @@
     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) {
-      assertEquals(abc.getVersion(), report.getReporter().getVersion());
-    }
-    
-    abc.setDisplayName("Herbert");
-    s.persist(abc);
-    List<Report> fresherReports = s.findReportsByEmployee(abc.getId());
-    assertEquals(reports.size(), fresherReports.size());
-    Integer expectedVersion = abc.getVersion() + 1;
-    for (Report report : fresherReports) {
-      assertEquals(abc.getId(), report.getReporter().getId());
-      assertEquals(expectedVersion, report.getReporter().getVersion());
-      assertEquals("Herbert", report.getReporter().getDisplayName());
-    }
-  }
-
   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;
-  }
 }
diff --git a/bikeshed/war/Stocks.css b/bikeshed/war/Stocks.css
index f241a27..8cd31d5 100644
--- a/bikeshed/war/Stocks.css
+++ b/bikeshed/war/Stocks.css
@@ -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 {