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 {