Web Queries

"Web queries" are Pivot's native means of communicating with remote data services. They are designed primarily to facilitate interaction with JSON-based REST services. However, they are sufficiently generic to support communication with any type of HTTP-based service, using any data format.

For example, the data presented by the Stock Tracker application is retrieved from Yahoo! Finance as a comma-separated value (CSV) file:

"AAPL","APPLE INC",171.06,169.59,172.17,166.00,+2.88,12995693
"AMZN","AMAZON.COM INC",72.54,72.35,73.83,70.52,+1.10,2748930
"EBAY","EBAY INC",27.09,27.35,27.44,27.04,-0.02,3426369

This data is returned by submitting an HTTP GET request to http://download.finance.yahoo.com/d/quotes.csv/ with an appropriate set of query string arguments. For example, the Stock Tracker application passes the following arguments to the service URL:

  • s - A comma-separated list of stock symbols representing the quotes to retrieve.
  • f - the requested format for the resulting CSV file; this is a string of characters representing the various data fields returned by the query. The format used by the Stock Tracker application is "snl1ohgc1v":
    • s - symbol
    • n - company name
    • l1 - most recent value
    • o - opening value
    • h - high value
    • g - low value
    • c1 - change percentage
    • v - volume

Note that this query actually returns more data than can be displayed in the table view. The other data fields are used by the quote detail view, which is discussed in more detail in the data binding section.

Constructing the Web Query

In order to display the stock quotes to the user, Stock Tracker must execute a web query to retrieve the data from the Yahoo! Finance web service and then populate the stock quote table view with the results of the query. The query is executed in the refreshTable() method, and the table is populated by a callback handler implemented as an anonymous inner class defined in this method. This code is defined in the StockTracker class.

First, the web query is created:

            
            getQuery = new GetQuery(SERVICE_HOSTNAME, SERVICE_PATH);
            
        

Then, the value of the "s" parameter is constructed by joining the values in the symbol list with commas, and the query arguments are applied:

            
            StringBuilder symbolsParameterBuilder = new StringBuilder();
            for (int i = 0, n = symbols.getLength(); i < n; i++) {
                if (i > 0) {
                    symbolsParameterBuilder.append(",");
                }

                symbolsParameterBuilder.append(symbols.get(i));
            }

            // Format:
            // s - symbol
            // n - company name
            // l1 - most recent value
            // o - opening value
            // h - high value
            // g - low value
            // c1 - change percentage
            // v - volume
            String symbolsParameter = symbolsParameterBuilder.toString();
            getQuery.getParameters().put("s", symbolsParameter);
            getQuery.getParameters().put("f", "snl1ohgc1v");
            
        

The resulting query URL would be similar to:

http://download.finance.yahoo.com/d/quotes.csv?s=aapl,amzn,ebay&f=snl1ohgc1v

Next, an instance of CSVSerializer is created and configured:

            
            CSVSerializer quoteSerializer = new CSVSerializer(StockQuote.class);
            quoteSerializer.setKeys("symbol",
                "companyName",
                "value",
                "openingValue",
                "highValue",
                "lowValue",
                "change",
                "volume");
            
        

By default, the GetQuery class uses an instance of org.apache.pivot.core.serialization.JSONSerializer to deserialize data returned by a GET request. However, the quote data from Yahoo! Finance is returned as a CSV file; CSVSerializer is an implementation of the org.apache.pivot.core.serialization.Serializer interface that parses a CSV data stream into a sequence of name/value pairs.

By default, CSVSerializer will create an instance of org.apache.pivot.collections.HashMap<String, Object> for each row it encounters in the CSV stream. However, a caller can specify the name of a different class as a constructor argument to CSVSerializer. This avoids the performance penalty of traversing the data twice: once to read it from the CSV stream and again to convert it to the appropriate type.

If the item class implements the org.apache.pivot.collections.Dictionary interface, the parsed values are put() directly into the item instance; otherwise, the item is wrapped in a org.apache.pivot.beans.BeanAdapter into which the values are put() (BeanAdapter is a handy class that allows a caller to treat a Java bean object as if it were a dictionary).

Stock Tracker uses the StockQuote class to represent the rows in the CSV file:

            
            public class StockQuote {
                private String symbol = null;
                private String companyName = null;
                private float value = 0;
                private float openingValue = 0;
                private float highValue = 0;
                private float lowValue = 0;
                private float change = 0;
                private float volume = 0;

                public String getSymbol() {
                    return symbol;
                }

                public void setSymbol(String symbol) {
                    this.symbol = symbol;
                }

                public String getCompanyName() {
                    return companyName;
                }

                public void setCompanyName(String companyName) {
                    this.companyName = companyName;
                }

                public float getValue() {
                    return value;
                }

                public void setValue(float value) {
                    this.value = value;
                }

                public void setValue(String value) {
                    try {
                        setValue(Float.parseFloat(value));
                    } catch(NumberFormatException exception) {
                        setValue(Float.NaN);
                    }
                }

                public float getOpeningValue() {
                    return openingValue;
                }

                public void setOpeningValue(float openingValue) {
                    this.openingValue = openingValue;
                }

                public void setOpeningValue(String openingValue) {
                    try {
                        setOpeningValue(Float.parseFloat(openingValue));
                    } catch(NumberFormatException exception) {
                        setOpeningValue(Float.NaN);
                    }
                }

                public float getHighValue() {
                    return highValue;
                }

                public void setHighValue(float highValue) {
                    this.highValue = highValue;
                }

                public void setHighValue(String highValue) {
                    try {
                        setHighValue(Float.parseFloat(highValue));
                    } catch(NumberFormatException exception) {
                        setHighValue(Float.NaN);
                    }
                }

                public float getLowValue() {
                    return lowValue;
                }

                public void setLowValue(float lowValue) {
                    this.lowValue = lowValue;
                }

                public void setLowValue(String lowValue) {
                    try {
                        setLowValue(Float.parseFloat(lowValue));
                    } catch(NumberFormatException exception) {
                        setLowValue(Float.NaN);
                    }
                }

                public float getChange() {
                    return change;
                }

                public void setChange(float change) {
                    this.change = change;
                }

                public void setChange(String change) {
                    try {
                        setChange(Float.parseFloat(change));
                    } catch(NumberFormatException exception) {
                        setChange(Float.NaN);
                    }
                }

                public float getVolume() {
                    return volume;
                }

                public void setVolume(float volume) {
                    this.volume = volume;
                }

                public void setVolume(String volume) {
                    try {
                        setVolume(Float.parseFloat(volume));
                    } catch(NumberFormatException exception) {
                        setVolume(Float.NaN);
                    }
                }
            }
            
        

The cell renderers assigned to the columns in the BXML file ensure that the data represented by this class is formatted and presented correctly.

Finally, the query is executed:

            
            getQuery.setSerializer(quoteSerializer);

            getQuery.execute(new TaskAdapter<Object>(new TaskListener<Object>() {
            ...
            }
            
        

The argument passed to the execute() method is a TaskAdapter wrapped around an anonymous inner class implementation of TaskListener<Object>. TaskListener is an interface defined in the org.apache.pivot.util.concurrent package and serves as a callback handler for asynchronous operations implemented by the org.apache.pivot.util.concurrent.Task class, of which GetQuery is a subclass. TaskAdapter is defined in the pivot.wtk package and ensures that the callback occurs on the UI thread (otherwise, the listener is called in the context of the background thread, which can produce non-deterministic results).

TaskListener defines two methods:

public void taskExecuted(Task<V> task);
public void executeFailed(Task<V> task);

The template parameter V is defined by the Task class and represents the (optional) return value of the operation. The first method is called if the task completes successfully, and the second is called if the task failed for any reason.

StockTracker's success handler is defined as follows:

            
                @Override
                public void taskExecuted(Task<Object> task) {
                    if (task == getQuery) {
                        List<Object> quotes = (List<Object>)task.getResult();

                        // Preserve any existing sort and selection
                        Sequence<?> selectedStocks = stocksTableView.getSelectedRows();

                        List<Object> tableData = (List<Object>)stocksTableView.getTableData();
                        Comparator<Object> comparator = tableData.getComparator();
                        quotes.setComparator(comparator);

                        stocksTableView.setTableData(quotes);

                        if (selectedStocks.getLength() > 0) {
                            // Select current indexes of selected stocks
                            for (int i = 0, n = selectedStocks.getLength(); i < n; i++) {
                                Object selectedStock = selectedStocks.get(i);

                                int index = 0;
                                for (Object stock : stocksTableView.getTableData()) {
                                    String symbol = JSON.get(stock, "symbol");
                                    String selectedSymbol = JSON.get(selectedStock, "symbol");

                                    if (symbol.equals(selectedSymbol)) {
                                        stocksTableView.addSelectedIndex(index);
                                        break;
                                    }

                                    index++;
                                }
                            }
                        } else {
                            if (quotes.getLength() > 0) {
                                stocksTableView.setSelectedIndex(0);
                            }
                        }

                        refreshDetail();

                        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG,
                            DateFormat.MEDIUM, Locale.getDefault());
                        lastUpdateLabel.setText(dateFormat.format(new Date()));

                        getQuery = null;
                    }
                }
            }
            
        

If the source of the event is the currently executing task, the handler does the following:

  • Caches the current sort and selection state of the quote table view

  • Gets the result of the query and casts it to the appropriate type (List<Object>)

  • Sets the list as the model data for the table view

  • Restores any selection state (which would have been lost when the model data was reset)

  • Updates the value of the "last updated" label to reflect the current time, in a manner appropriate for the current locale

In the case of a failure, the handler simply logs the exception to the console:

            
            @Override
            public void executeFailed(Task<Object> task) {
                if (task == getQuery) {
                    System.err.println(task.getFault());
                    getQuery = null;
                }
            }
            
        

This example demonstrates the use of GetQuery only, but Pivot also provides support for HTTP POST, PUT, and DELETE operations using the PostQuery, PutQuery, and DeleteQuery classes, respectively. This makes it very easy to built a complete REST client using Pivot.

Next: Data Binding